Merge lp:~julian-edwards/launchpad/cancel-build-bug-173018-ui-part3 into lp:launchpad

Proposed by Julian Edwards
Status: Merged
Approved by: Julian Edwards
Approved revision: 14222
Merged at revision: 14224
Proposed branch: lp:~julian-edwards/launchpad/cancel-build-bug-173018-ui-part3
Merge into: lp:launchpad
Diff against target: 269 lines (+170/-3)
4 files modified
lib/lp/soyuz/interfaces/binarypackagebuild.py (+23/-0)
lib/lp/soyuz/model/binarypackagebuild.py (+28/-0)
lib/lp/soyuz/stories/webservice/xx-builds.txt (+2/-0)
lib/lp/soyuz/tests/test_binarypackagebuild.py (+117/-3)
To merge this branch: bzr merge lp:~julian-edwards/launchpad/cancel-build-bug-173018-ui-part3
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+80888@code.launchpad.net

Commit message

[r=gmb][bug=173018][incr] Add API functionality to cancel virtual builds.

Description of the change

Add BinaryPackageBuild.cancel() and can_be_cancelled property, and export both to the webservice.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Hi Julian,

Nice branch, just the one comment, otherwise r=me

186 + def test_cancel_not_in_progress(self):
187 + # Testing the cancel() method for a pending build.

195 + def test_cancel_in_progress(self):
196 + # Testing the cancel() method for a building build.

E_LAZY_DEVELOPER: I can figure out what you're testing from the method names - the comments more-or-less repeat that information - but I don't know what the expected behaviour is unless I read the code. Please update the comments so that they tell me what _should_ happen.

review: Approve (code)
14224. By Julian Edwards

suggestion from gmb in review

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/soyuz/interfaces/binarypackagebuild.py'
--- lib/lp/soyuz/interfaces/binarypackagebuild.py 2011-06-16 20:12:00 +0000
+++ lib/lp/soyuz/interfaces/binarypackagebuild.py 2011-11-01 14:12:30 +0000
@@ -27,6 +27,7 @@
27 export_as_webservice_entry,27 export_as_webservice_entry,
28 export_write_operation,28 export_write_operation,
29 exported,29 exported,
30 operation_for_version,
30 operation_parameters,31 operation_parameters,
31 )32 )
32from lazr.restful.fields import Reference33from lazr.restful.fields import Reference
@@ -119,6 +120,12 @@
119 description=_(120 description=_(
120 "Whether or not this build record can be retried.")))121 "Whether or not this build record can be retried.")))
121122
123 can_be_cancelled = exported(
124 Bool(
125 title=_("Can Be Cancelled"), required=False, readonly=True,
126 description=_(
127 "Whether or not this build record can be cancelled.")))
128
122 is_virtualized = Attribute(129 is_virtualized = Attribute(
123 "Whether or not this build requires a virtual build host or not.")130 "Whether or not this build requires a virtual build host or not.")
124131
@@ -223,6 +230,22 @@
223 non-scored BuildQueue entry is created for it.230 non-scored BuildQueue entry is created for it.
224 """231 """
225232
233 @export_write_operation()
234 @operation_for_version("devel")
235 def cancel():
236 """Cancel the build if it is either pending or in progress.
237
238 Call the can_be_cancelled() method prior to this one to find out if
239 cancelling the build is possible.
240
241 If the build is in progress, it is marked as CANCELLING until the
242 buildd manager terminates the build and marks it CANCELLED. If the
243 build is not in progress, it is marked CANCELLED immediately and is
244 removed from the build queue.
245
246 If the build is not in a cancellable state, this method is a no-op.
247 """
248
226249
227class IBinaryPackageBuildAdmin(Interface):250class IBinaryPackageBuildAdmin(Interface):
228 """A Build interface for items requiring launchpad.Admin."""251 """A Build interface for items requiring launchpad.Admin."""
229252
=== modified file 'lib/lp/soyuz/model/binarypackagebuild.py'
--- lib/lp/soyuz/model/binarypackagebuild.py 2011-09-23 15:46:11 +0000
+++ lib/lp/soyuz/model/binarypackagebuild.py 2011-11-01 14:12:30 +0000
@@ -349,6 +349,20 @@
349 """See `IBuild`."""349 """See `IBuild`."""
350 return self.status is BuildStatus.NEEDSBUILD350 return self.status is BuildStatus.NEEDSBUILD
351351
352 @property
353 def can_be_cancelled(self):
354 """See `IBuild`."""
355 if not self.buildqueue_record:
356 return False
357 if self.buildqueue_record.virtualized is False:
358 return False
359
360 cancellable_statuses = [
361 BuildStatus.BUILDING,
362 BuildStatus.NEEDSBUILD,
363 ]
364 return self.status in cancellable_statuses
365
352 def retry(self):366 def retry(self):
353 """See `IBuild`."""367 """See `IBuild`."""
354 assert self.can_be_retried, "Build %s cannot be retried" % self.id368 assert self.can_be_retried, "Build %s cannot be retried" % self.id
@@ -377,6 +391,20 @@
377 else:391 else:
378 return self.buildqueue_record.lastscore392 return self.buildqueue_record.lastscore
379393
394 def cancel(self):
395 """See `IBinaryPackageBuild`."""
396 if not self.can_be_cancelled:
397 return
398
399 # If the build is currently building we need to tell the
400 # buildd-manager to terminate it.
401 if self.status == BuildStatus.BUILDING:
402 self.status = BuildStatus.CANCELLING
403 return
404
405 # Otherwise we can cancel it here.
406 self.buildqueue_record.cancel()
407
380 def makeJob(self):408 def makeJob(self):
381 """See `IBuildFarmJob`."""409 """See `IBuildFarmJob`."""
382 store = Store.of(self)410 store = Store.of(self)
383411
=== modified file 'lib/lp/soyuz/stories/webservice/xx-builds.txt'
--- lib/lp/soyuz/stories/webservice/xx-builds.txt 2011-05-04 02:45:25 +0000
+++ lib/lp/soyuz/stories/webservice/xx-builds.txt 2011-11-01 14:12:30 +0000
@@ -42,6 +42,7 @@
42 >>> pprint_entry(builds['entries'][0])42 >>> pprint_entry(builds['entries'][0])
43 arch_tag: u'i386'43 arch_tag: u'i386'
44 archive_link: u'http://.../beta/~cprov/+archive/ppa'44 archive_link: u'http://.../beta/~cprov/+archive/ppa'
45 can_be_cancelled: False
45 can_be_rescored: False46 can_be_rescored: False
46 can_be_retried: True47 can_be_retried: True
47 changesfile_url: None48 changesfile_url: None
@@ -71,6 +72,7 @@
71 archive_link: u'http://.../~cprov/+archive/ppa'72 archive_link: u'http://.../~cprov/+archive/ppa'
72 build_log_url: u'http://.../~cprov/+archive/ppa/+build/26/+files/netapplet-1.0.0.tar.gz'73 build_log_url: u'http://.../~cprov/+archive/ppa/+build/26/+files/netapplet-1.0.0.tar.gz'
73 buildstate: u'Failed to build'74 buildstate: u'Failed to build'
75 can_be_cancelled: False
74 can_be_rescored: False76 can_be_rescored: False
75 can_be_retried: True77 can_be_retried: True
76 changesfile_url: None78 changesfile_url: None
7779
=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuild.py'
--- lib/lp/soyuz/tests/test_binarypackagebuild.py 2011-05-04 06:37:50 +0000
+++ lib/lp/soyuz/tests/test_binarypackagebuild.py 2011-11-01 14:12:30 +0000
@@ -15,7 +15,15 @@
1515
16from twisted.trial.unittest import TestCase as TrialTestCase16from twisted.trial.unittest import TestCase as TrialTestCase
1717
18from canonical.testing.layers import LaunchpadZopelessLayer18from canonical.launchpad.testing.pages import (
19 webservice_for_person,
20 )
21from canonical.launchpad.webapp.interaction import ANONYMOUS
22from canonical.launchpad.webapp.interfaces import OAuthPermission
23from canonical.testing.layers import (
24 DatabaseFunctionalLayer,
25 LaunchpadZopelessLayer,
26 )
19from lp.buildmaster.enums import BuildStatus27from lp.buildmaster.enums import BuildStatus
20from lp.buildmaster.interfaces.builder import IBuilderSet28from lp.buildmaster.interfaces.builder import IBuilderSet
21from lp.buildmaster.interfaces.buildqueue import IBuildQueue29from lp.buildmaster.interfaces.buildqueue import IBuildQueue
@@ -27,7 +35,10 @@
27 TestHandleStatusMixin,35 TestHandleStatusMixin,
28 )36 )
29from lp.services.job.model.job import Job37from lp.services.job.model.job import Job
30from lp.soyuz.enums import PackagePublishingStatus38from lp.soyuz.enums import (
39 ArchivePurpose,
40 PackagePublishingStatus,
41 )
31from lp.soyuz.interfaces.binarypackagebuild import (42from lp.soyuz.interfaces.binarypackagebuild import (
32 IBinaryPackageBuild,43 IBinaryPackageBuild,
33 IBinaryPackageBuildSet,44 IBinaryPackageBuildSet,
@@ -39,7 +50,12 @@
39from lp.soyuz.model.buildpackagejob import BuildPackageJob50from lp.soyuz.model.buildpackagejob import BuildPackageJob
40from lp.soyuz.model.processor import ProcessorFamilySet51from lp.soyuz.model.processor import ProcessorFamilySet
41from lp.soyuz.tests.test_publishing import SoyuzTestPublisher52from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
42from lp.testing import TestCaseWithFactory53from lp.testing import (
54 api_url,
55 login,
56 logout,
57 TestCaseWithFactory,
58 )
4359
4460
45class TestBinaryPackageBuild(TestCaseWithFactory):61class TestBinaryPackageBuild(TestCaseWithFactory):
@@ -170,6 +186,48 @@
170 self.assertEquals("Somebody <somebody@ubuntu.com>",186 self.assertEquals("Somebody <somebody@ubuntu.com>",
171 self.build.getUploader(MockChanges()))187 self.build.getUploader(MockChanges()))
172188
189 def test_can_be_cancelled(self):
190 # For all states that can be cancelled, assert can_be_cancelled
191 # returns True.
192 ok_cases = [
193 BuildStatus.BUILDING,
194 BuildStatus.NEEDSBUILD,
195 ]
196 for status in BuildStatus:
197 if status in ok_cases:
198 self.assertTrue(self.build.can_be_cancelled)
199 else:
200 self.assertFalse(self.build.can_be_cancelled)
201
202 def test_can_be_cancelled_virtuality(self):
203 # Only virtual builds can be cancelled.
204 bq = removeSecurityProxy(self.build.queueBuild())
205 bq.virtualized = True
206 self.assertTrue(self.build.can_be_cancelled)
207 bq.virtualized = False
208 self.assertFalse(self.build.can_be_cancelled)
209
210 def test_cancel_not_in_progress(self):
211 # Testing the cancel() method for a pending build should leave
212 # it in the CANCELLED state.
213 ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
214 build = self.factory.makeBinaryPackageBuild(archive=ppa)
215 build.queueBuild()
216 build.cancel()
217 self.assertEqual(BuildStatus.CANCELLED, build.status)
218 self.assertIs(None, build.buildqueue_record)
219
220 def test_cancel_in_progress(self):
221 # Testing the cancel() method for a building build should leave
222 # it in the CANCELLING state.
223 ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
224 build = self.factory.makeBinaryPackageBuild(archive=ppa)
225 bq = build.queueBuild()
226 build.status = BuildStatus.BUILDING
227 build.cancel()
228 self.assertEqual(BuildStatus.CANCELLING, build.status)
229 self.assertEqual(bq, build.buildqueue_record)
230
173231
174class TestBuildUpdateDependencies(TestCaseWithFactory):232class TestBuildUpdateDependencies(TestCaseWithFactory):
175233
@@ -503,3 +561,59 @@
503class TestHandleStatusForBinaryPackageBuild(561class TestHandleStatusForBinaryPackageBuild(
504 MakeBinaryPackageBuildMixin, TestHandleStatusMixin, TrialTestCase):562 MakeBinaryPackageBuildMixin, TestHandleStatusMixin, TrialTestCase):
505 """IPackageBuild.handleStatus works with binary builds."""563 """IPackageBuild.handleStatus works with binary builds."""
564
565
566class TestBinaryPackageBuildWebservice(TestCaseWithFactory):
567 """Test cases for BinaryPackageBuild on the webservice.
568
569 NB. Note that most tests are currently in
570 lib/lp/soyuz/stories/webservice/xx-builds.txt but unit tests really
571 ought to be here instead.
572 """
573
574 layer = DatabaseFunctionalLayer
575
576 def setUp(self):
577 super(TestBinaryPackageBuildWebservice, self).setUp()
578 self.ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
579 self.build = self.factory.makeBinaryPackageBuild(archive=self.ppa)
580 self.webservice = webservice_for_person(
581 self.ppa.owner, permission=OAuthPermission.WRITE_PUBLIC)
582 login(ANONYMOUS)
583
584 def test_can_be_cancelled_is_exported(self):
585 # Check that the can_be_cancelled property is exported.
586 expected = self.build.can_be_cancelled
587 entry_url = api_url(self.build)
588 logout()
589 entry = self.webservice.get(
590 entry_url, api_version='devel').jsonBody()
591 self.assertEqual(expected, entry['can_be_cancelled'])
592
593 def test_cancel_is_exported(self):
594 # Check that the cancel() named op is exported.
595 build_url = api_url(self.build)
596 self.build.queueBuild()
597 logout()
598 entry = self.webservice.get(
599 build_url, api_version='devel').jsonBody()
600 response = self.webservice.named_post(
601 entry['self_link'], 'cancel', api_version='devel')
602 self.assertEqual(200, response.status)
603 entry = self.webservice.get(
604 build_url, api_version='devel').jsonBody()
605 self.assertEqual(BuildStatus.CANCELLED.title, entry['buildstate'])
606
607 def test_cancel_security(self):
608 # Check that unauthorised users cannot call cancel()
609 build_url = api_url(self.build)
610 person = self.factory.makePerson()
611 webservice = webservice_for_person(
612 person, permission=OAuthPermission.WRITE_PUBLIC)
613 logout()
614
615 entry = webservice.get(
616 build_url, api_version='devel').jsonBody()
617 response = webservice.named_post(
618 entry['self_link'], 'cancel', api_version='devel')
619 self.assertEqual(401, response.status)