Merge lp:~cjwatson/launchpad/snap-request-builds-job into lp:launchpad
- snap-request-builds-job
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18723 | ||||
Proposed branch: | lp:~cjwatson/launchpad/snap-request-builds-job | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/snap-parse-architectures | ||||
Diff against target: |
855 lines (+680/-9) 9 files modified
database/schema/security.cfg (+15/-2) lib/lp/services/config/schema-lazr.conf (+6/-0) lib/lp/snappy/configure.zcml (+13/-1) lib/lp/snappy/interfaces/snap.py (+15/-0) lib/lp/snappy/interfaces/snapjob.py (+97/-0) lib/lp/snappy/model/snap.py (+47/-6) lib/lp/snappy/model/snapjob.py (+271/-0) lib/lp/snappy/tests/test_snap.py (+61/-0) lib/lp/snappy/tests/test_snapjob.py (+155/-0) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-request-builds-job | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+348058@code.launchpad.net |
Commit message
Add a job to request builds of a snap for relevant architectures.
Description of the change
See also https:/
I considered whether to use the existing request-
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'database/schema/security.cfg' | |||
2 | --- database/schema/security.cfg 2018-05-22 06:42:35 +0000 | |||
3 | +++ database/schema/security.cfg 2018-06-15 13:00:33 +0000 | |||
4 | @@ -1,4 +1,4 @@ | |||
6 | 1 | # Copyright 2009-2017 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | # | 3 | # |
9 | 4 | # Possible permissions: SELECT, INSERT, UPDATE, EXECUTE | 4 | # Possible permissions: SELECT, INSERT, UPDATE, EXECUTE |
10 | @@ -287,6 +287,7 @@ | |||
11 | 287 | public.snapbuild = SELECT, INSERT, UPDATE, DELETE | 287 | public.snapbuild = SELECT, INSERT, UPDATE, DELETE |
12 | 288 | public.snapbuildjob = SELECT, INSERT, UPDATE, DELETE | 288 | public.snapbuildjob = SELECT, INSERT, UPDATE, DELETE |
13 | 289 | public.snapfile = SELECT, INSERT, UPDATE, DELETE | 289 | public.snapfile = SELECT, INSERT, UPDATE, DELETE |
14 | 290 | public.snapjob = SELECT, INSERT, UPDATE, DELETE | ||
15 | 290 | public.snappydistroseries = SELECT, INSERT, UPDATE, DELETE | 291 | public.snappydistroseries = SELECT, INSERT, UPDATE, DELETE |
16 | 291 | public.snappyseries = SELECT, INSERT, UPDATE, DELETE | 292 | public.snappyseries = SELECT, INSERT, UPDATE, DELETE |
17 | 292 | public.sourcepackageformatselection = SELECT | 293 | public.sourcepackageformatselection = SELECT |
18 | @@ -2537,21 +2538,33 @@ | |||
19 | 2537 | groups=script | 2538 | groups=script |
20 | 2538 | public.account = SELECT | 2539 | public.account = SELECT |
21 | 2539 | public.archive = SELECT | 2540 | public.archive = SELECT |
22 | 2541 | public.branch = SELECT | ||
23 | 2540 | public.builder = SELECT | 2542 | public.builder = SELECT |
24 | 2543 | public.buildfarmjob = SELECT, INSERT | ||
25 | 2544 | public.buildqueue = SELECT, INSERT, UPDATE | ||
26 | 2541 | public.distribution = SELECT | 2545 | public.distribution = SELECT |
27 | 2542 | public.distroarchseries = SELECT | 2546 | public.distroarchseries = SELECT |
28 | 2543 | public.distroseries = SELECT | 2547 | public.distroseries = SELECT |
29 | 2544 | public.emailaddress = SELECT | 2548 | public.emailaddress = SELECT |
30 | 2549 | public.gitref = SELECT | ||
31 | 2550 | public.gitrepository = SELECT | ||
32 | 2545 | public.job = SELECT, INSERT, UPDATE | 2551 | public.job = SELECT, INSERT, UPDATE |
33 | 2546 | public.libraryfilealias = SELECT | 2552 | public.libraryfilealias = SELECT |
34 | 2547 | public.libraryfilecontent = SELECT | 2553 | public.libraryfilecontent = SELECT |
35 | 2548 | public.person = SELECT | 2554 | public.person = SELECT |
36 | 2549 | public.personsettings = SELECT | 2555 | public.personsettings = SELECT |
37 | 2556 | public.pocketchroot = SELECT | ||
38 | 2557 | public.processor = SELECT | ||
39 | 2558 | public.product = SELECT | ||
40 | 2550 | public.snap = SELECT, UPDATE | 2559 | public.snap = SELECT, UPDATE |
42 | 2551 | public.snapbuild = SELECT, UPDATE | 2560 | public.snaparch = SELECT |
43 | 2561 | public.snapbuild = SELECT, INSERT, UPDATE | ||
44 | 2552 | public.snapbuildjob = SELECT, UPDATE | 2562 | public.snapbuildjob = SELECT, UPDATE |
45 | 2553 | public.snapfile = SELECT | 2563 | public.snapfile = SELECT |
46 | 2564 | public.snapjob = SELECT, UPDATE | ||
47 | 2554 | public.snappyseries = SELECT | 2565 | public.snappyseries = SELECT |
48 | 2566 | public.sourcepackagename = SELECT | ||
49 | 2555 | public.teammembership = SELECT | 2567 | public.teammembership = SELECT |
50 | 2568 | public.teamparticipation = SELECT | ||
51 | 2556 | public.webhook = SELECT | 2569 | public.webhook = SELECT |
52 | 2557 | public.webhookjob = SELECT, INSERT | 2570 | public.webhookjob = SELECT, INSERT |
53 | 2558 | 2571 | ||
54 | === modified file 'lib/lp/services/config/schema-lazr.conf' | |||
55 | --- lib/lp/services/config/schema-lazr.conf 2018-06-15 13:00:33 +0000 | |||
56 | +++ lib/lp/services/config/schema-lazr.conf 2018-06-15 13:00:33 +0000 | |||
57 | @@ -1860,6 +1860,7 @@ | |||
58 | 1860 | IRemoveArtifactSubscriptionsJobSource, | 1860 | IRemoveArtifactSubscriptionsJobSource, |
59 | 1861 | ISelfRenewalNotificationJobSource, | 1861 | ISelfRenewalNotificationJobSource, |
60 | 1862 | ISevenDayCommercialExpirationJobSource, | 1862 | ISevenDayCommercialExpirationJobSource, |
61 | 1863 | ISnapRequestBuildsJobSource, | ||
62 | 1863 | ISnapStoreUploadJobSource, | 1864 | ISnapStoreUploadJobSource, |
63 | 1864 | ITeamInvitationNotificationJobSource, | 1865 | ITeamInvitationNotificationJobSource, |
64 | 1865 | ITeamJoinNotificationJobSource, | 1866 | ITeamJoinNotificationJobSource, |
65 | @@ -2002,6 +2003,11 @@ | |||
66 | 2002 | dbuser: product-job | 2003 | dbuser: product-job |
67 | 2003 | crontab_group: MAIN | 2004 | crontab_group: MAIN |
68 | 2004 | 2005 | ||
69 | 2006 | [ISnapRequestBuildsJobSource] | ||
70 | 2007 | module: lp.snappy.interfaces.snapjob | ||
71 | 2008 | dbuser: snap-build-job | ||
72 | 2009 | crontab_group: MAIN | ||
73 | 2010 | |||
74 | 2005 | [ISnapStoreUploadJobSource] | 2011 | [ISnapStoreUploadJobSource] |
75 | 2006 | module: lp.snappy.interfaces.snapbuildjob | 2012 | module: lp.snappy.interfaces.snapbuildjob |
76 | 2007 | dbuser: snap-build-job | 2013 | dbuser: snap-build-job |
77 | 2008 | 2014 | ||
78 | === modified file 'lib/lp/snappy/configure.zcml' | |||
79 | --- lib/lp/snappy/configure.zcml 2017-03-20 00:03:52 +0000 | |||
80 | +++ lib/lp/snappy/configure.zcml 2018-06-15 13:00:33 +0000 | |||
81 | @@ -1,4 +1,4 @@ | |||
83 | 1 | <!-- Copyright 2015-2017 Canonical Ltd. This software is licensed under the | 1 | <!-- Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
84 | 2 | GNU Affero General Public License version 3 (see the file LICENSE). | 2 | GNU Affero General Public License version 3 (see the file LICENSE). |
85 | 3 | --> | 3 | --> |
86 | 4 | 4 | ||
87 | @@ -127,6 +127,18 @@ | |||
88 | 127 | </securedutility> | 127 | </securedutility> |
89 | 128 | 128 | ||
90 | 129 | <!-- Snap-related jobs --> | 129 | <!-- Snap-related jobs --> |
91 | 130 | <class class="lp.snappy.model.snapjob.SnapJob"> | ||
92 | 131 | <allow interface="lp.snappy.interfaces.snapjob.ISnapJob" /> | ||
93 | 132 | </class> | ||
94 | 133 | <securedutility | ||
95 | 134 | component="lp.snappy.model.snapjob.SnapRequestBuildsJob" | ||
96 | 135 | provides="lp.snappy.interfaces.snapjob.ISnapRequestBuildsJobSource"> | ||
97 | 136 | <allow interface="lp.snappy.interfaces.snapjob.ISnapRequestBuildsJobSource" /> | ||
98 | 137 | </securedutility> | ||
99 | 138 | <class class="lp.snappy.model.snapjob.SnapRequestBuildsJob"> | ||
100 | 139 | <allow interface="lp.snappy.interfaces.snapjob.ISnapJob" /> | ||
101 | 140 | <allow interface="lp.snappy.interfaces.snapjob.ISnapRequestBuildsJob" /> | ||
102 | 141 | </class> | ||
103 | 130 | <class class="lp.snappy.model.snapbuildjob.SnapBuildJob"> | 142 | <class class="lp.snappy.model.snapbuildjob.SnapBuildJob"> |
104 | 131 | <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" /> | 143 | <allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" /> |
105 | 132 | </class> | 144 | </class> |
106 | 133 | 145 | ||
107 | === modified file 'lib/lp/snappy/interfaces/snap.py' | |||
108 | --- lib/lp/snappy/interfaces/snap.py 2018-06-15 13:00:33 +0000 | |||
109 | +++ lib/lp/snappy/interfaces/snap.py 2018-06-15 13:00:33 +0000 | |||
110 | @@ -316,6 +316,21 @@ | |||
111 | 316 | :return: `ISnapBuild`. | 316 | :return: `ISnapBuild`. |
112 | 317 | """ | 317 | """ |
113 | 318 | 318 | ||
114 | 319 | def requestBuildsFromJob(requester, archive, pocket, channels=None, | ||
115 | 320 | logger=None): | ||
116 | 321 | """Synchronous part of `Snap.requestBuilds`. | ||
117 | 322 | |||
118 | 323 | Request that the snap package be built for relevant architectures. | ||
119 | 324 | |||
120 | 325 | :param requester: The person requesting the builds. | ||
121 | 326 | :param archive: The IArchive to associate the builds with. | ||
122 | 327 | :param pocket: The pocket that should be targeted. | ||
123 | 328 | :param channels: A dictionary mapping snap names to channels to use | ||
124 | 329 | for these builds. | ||
125 | 330 | :param logger: An optional logger. | ||
126 | 331 | :return: A sequence of `ISnapBuild` instances. | ||
127 | 332 | """ | ||
128 | 333 | |||
129 | 319 | @operation_parameters( | 334 | @operation_parameters( |
130 | 320 | snap_build_ids=List( | 335 | snap_build_ids=List( |
131 | 321 | title=_("A list of snap build ids."), | 336 | title=_("A list of snap build ids."), |
132 | 322 | 337 | ||
133 | === added file 'lib/lp/snappy/interfaces/snapjob.py' | |||
134 | --- lib/lp/snappy/interfaces/snapjob.py 1970-01-01 00:00:00 +0000 | |||
135 | +++ lib/lp/snappy/interfaces/snapjob.py 2018-06-15 13:00:33 +0000 | |||
136 | @@ -0,0 +1,97 @@ | |||
137 | 1 | # Copyright 2018 Canonical Ltd. This software is licensed under the | ||
138 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
139 | 3 | |||
140 | 4 | """Snap job interfaces.""" | ||
141 | 5 | |||
142 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
143 | 7 | |||
144 | 8 | __metaclass__ = type | ||
145 | 9 | __all__ = [ | ||
146 | 10 | 'ISnapJob', | ||
147 | 11 | 'ISnapRequestBuildsJob', | ||
148 | 12 | 'ISnapRequestBuildsJobSource', | ||
149 | 13 | ] | ||
150 | 14 | |||
151 | 15 | from lazr.restful.fields import Reference | ||
152 | 16 | from zope.interface import ( | ||
153 | 17 | Attribute, | ||
154 | 18 | Interface, | ||
155 | 19 | ) | ||
156 | 20 | from zope.schema import ( | ||
157 | 21 | Choice, | ||
158 | 22 | Dict, | ||
159 | 23 | List, | ||
160 | 24 | TextLine, | ||
161 | 25 | ) | ||
162 | 26 | |||
163 | 27 | from lp import _ | ||
164 | 28 | from lp.registry.interfaces.person import IPerson | ||
165 | 29 | from lp.registry.interfaces.pocket import PackagePublishingPocket | ||
166 | 30 | from lp.services.job.interfaces.job import ( | ||
167 | 31 | IJob, | ||
168 | 32 | IJobSource, | ||
169 | 33 | IRunnableJob, | ||
170 | 34 | ) | ||
171 | 35 | from lp.snappy.interfaces.snap import ISnap | ||
172 | 36 | from lp.snappy.interfaces.snapbuild import ISnapBuild | ||
173 | 37 | from lp.soyuz.interfaces.archive import IArchive | ||
174 | 38 | |||
175 | 39 | |||
176 | 40 | class ISnapJob(Interface): | ||
177 | 41 | """A job related to a snap package.""" | ||
178 | 42 | |||
179 | 43 | job = Reference( | ||
180 | 44 | title=_("The common Job attributes."), schema=IJob, | ||
181 | 45 | required=True, readonly=True) | ||
182 | 46 | |||
183 | 47 | snap = Reference( | ||
184 | 48 | title=_("The snap package to use for this job."), | ||
185 | 49 | schema=ISnap, required=True, readonly=True) | ||
186 | 50 | |||
187 | 51 | metadata = Attribute(_("A dict of data about the job.")) | ||
188 | 52 | |||
189 | 53 | |||
190 | 54 | class ISnapRequestBuildsJob(IRunnableJob): | ||
191 | 55 | """A Job that processes a request for builds of a snap package.""" | ||
192 | 56 | |||
193 | 57 | requester = Reference( | ||
194 | 58 | title=_("The person requesting the builds."), schema=IPerson, | ||
195 | 59 | required=True, readonly=True) | ||
196 | 60 | |||
197 | 61 | archive = Reference( | ||
198 | 62 | title=_("The archive to associate the builds with."), schema=IArchive, | ||
199 | 63 | required=True, readonly=True) | ||
200 | 64 | |||
201 | 65 | pocket = Choice( | ||
202 | 66 | title=_("The pocket that should be targeted."), | ||
203 | 67 | vocabulary=PackagePublishingPocket, required=True, readonly=True) | ||
204 | 68 | |||
205 | 69 | channels = Dict( | ||
206 | 70 | title=_("Source snap channels to use for these builds."), | ||
207 | 71 | description=_( | ||
208 | 72 | "A dictionary mapping snap names to channels to use for these " | ||
209 | 73 | "builds. Currently only 'core' and 'snapcraft' keys are " | ||
210 | 74 | "supported."), | ||
211 | 75 | key_type=TextLine(), required=False, readonly=True) | ||
212 | 76 | |||
213 | 77 | error_message = TextLine( | ||
214 | 78 | title=_("Error message resulting from running this job."), | ||
215 | 79 | required=False, readonly=True) | ||
216 | 80 | |||
217 | 81 | builds = List( | ||
218 | 82 | title=_("The builds created by this request."), | ||
219 | 83 | value_type=Reference(schema=ISnapBuild), required=True, readonly=True) | ||
220 | 84 | |||
221 | 85 | |||
222 | 86 | class ISnapRequestBuildsJobSource(IJobSource): | ||
223 | 87 | |||
224 | 88 | def create(snap, requester, archive, pocket, channels): | ||
225 | 89 | """Request builds of a snap package. | ||
226 | 90 | |||
227 | 91 | :param snap: The snap package to build. | ||
228 | 92 | :param requester: The person requesting the builds. | ||
229 | 93 | :param archive: The IArchive to associate the builds with. | ||
230 | 94 | :param pocket: The pocket that should be targeted. | ||
231 | 95 | :param channels: A dictionary mapping snap names to channels to use | ||
232 | 96 | for these builds. | ||
233 | 97 | """ | ||
234 | 0 | 98 | ||
235 | === modified file 'lib/lp/snappy/model/snap.py' | |||
236 | --- lib/lp/snappy/model/snap.py 2018-06-15 13:00:33 +0000 | |||
237 | +++ lib/lp/snappy/model/snap.py 2018-06-15 13:00:33 +0000 | |||
238 | @@ -6,10 +6,12 @@ | |||
239 | 6 | 'Snap', | 6 | 'Snap', |
240 | 7 | ] | 7 | ] |
241 | 8 | 8 | ||
242 | 9 | from collections import OrderedDict | ||
243 | 9 | from datetime import ( | 10 | from datetime import ( |
244 | 10 | datetime, | 11 | datetime, |
245 | 11 | timedelta, | 12 | timedelta, |
246 | 12 | ) | 13 | ) |
247 | 14 | from operator import attrgetter | ||
248 | 13 | from urlparse import urlsplit | 15 | from urlparse import urlsplit |
249 | 14 | 16 | ||
250 | 15 | from pymacaroons import Macaroon | 17 | from pymacaroons import Macaroon |
251 | @@ -116,6 +118,7 @@ | |||
252 | 116 | from lp.services.webapp.interfaces import ILaunchBag | 118 | from lp.services.webapp.interfaces import ILaunchBag |
253 | 117 | from lp.services.webhooks.interfaces import IWebhookSet | 119 | from lp.services.webhooks.interfaces import IWebhookSet |
254 | 118 | from lp.services.webhooks.model import WebhookTargetMixin | 120 | from lp.services.webhooks.model import WebhookTargetMixin |
255 | 121 | from lp.snappy.adapters.buildarch import determine_architectures_to_build | ||
256 | 119 | from lp.snappy.interfaces.snap import ( | 122 | from lp.snappy.interfaces.snap import ( |
257 | 120 | BadSnapSearchContext, | 123 | BadSnapSearchContext, |
258 | 121 | BadSnapSource, | 124 | BadSnapSource, |
259 | @@ -463,21 +466,25 @@ | |||
260 | 463 | return False | 466 | return False |
261 | 464 | return True | 467 | return True |
262 | 465 | 468 | ||
266 | 466 | def requestBuild(self, requester, archive, distro_arch_series, pocket, | 469 | def _checkRequestBuild(self, requester, archive): |
267 | 467 | channels=None): | 470 | """May `requester` request builds of this snap from `archive`?""" |
265 | 468 | """See `ISnap`.""" | ||
268 | 469 | if not requester.inTeam(self.owner): | 471 | if not requester.inTeam(self.owner): |
269 | 470 | raise SnapNotOwner( | 472 | raise SnapNotOwner( |
270 | 471 | "%s cannot create snap package builds owned by %s." % | 473 | "%s cannot create snap package builds owned by %s." % |
271 | 472 | (requester.displayname, self.owner.displayname)) | 474 | (requester.displayname, self.owner.displayname)) |
272 | 473 | if not archive.enabled: | 475 | if not archive.enabled: |
273 | 474 | raise ArchiveDisabled(archive.displayname) | 476 | raise ArchiveDisabled(archive.displayname) |
274 | 475 | if distro_arch_series not in self.getAllowedArchitectures(): | ||
275 | 476 | raise SnapBuildDisallowedArchitecture(distro_arch_series) | ||
276 | 477 | if archive.private and self.owner != archive.owner: | 477 | if archive.private and self.owner != archive.owner: |
277 | 478 | # See rationale in `SnapBuildArchiveOwnerMismatch` docstring. | 478 | # See rationale in `SnapBuildArchiveOwnerMismatch` docstring. |
278 | 479 | raise SnapBuildArchiveOwnerMismatch() | 479 | raise SnapBuildArchiveOwnerMismatch() |
279 | 480 | 480 | ||
280 | 481 | def requestBuild(self, requester, archive, distro_arch_series, pocket, | ||
281 | 482 | channels=None): | ||
282 | 483 | """See `ISnap`.""" | ||
283 | 484 | self._checkRequestBuild(requester, archive) | ||
284 | 485 | if distro_arch_series not in self.getAllowedArchitectures(): | ||
285 | 486 | raise SnapBuildDisallowedArchitecture(distro_arch_series) | ||
286 | 487 | |||
287 | 481 | pending = IStore(self).find( | 488 | pending = IStore(self).find( |
288 | 482 | SnapBuild, | 489 | SnapBuild, |
289 | 483 | SnapBuild.snap_id == self.id, | 490 | SnapBuild.snap_id == self.id, |
290 | @@ -495,8 +502,42 @@ | |||
291 | 495 | build.queueBuild() | 502 | build.queueBuild() |
292 | 496 | return build | 503 | return build |
293 | 497 | 504 | ||
294 | 505 | def requestBuildsFromJob(self, requester, archive, pocket, channels=None, | ||
295 | 506 | logger=None): | ||
296 | 507 | """See `ISnap`.""" | ||
297 | 508 | snapcraft_data = removeSecurityProxy( | ||
298 | 509 | getUtility(ISnapSet).getSnapcraftYaml(self)) | ||
299 | 510 | # Sort by Processor.id for determinism. This is chosen to be the | ||
300 | 511 | # same order as in BinaryPackageBuildSet.createForSource, to | ||
301 | 512 | # minimise confusion. | ||
302 | 513 | supported_arches = OrderedDict( | ||
303 | 514 | (das.architecturetag, das) for das in sorted( | ||
304 | 515 | self.getAllowedArchitectures(), | ||
305 | 516 | key=attrgetter("processor.id"))) | ||
306 | 517 | architectures_to_build = determine_architectures_to_build( | ||
307 | 518 | snapcraft_data, supported_arches.keys()) | ||
308 | 519 | |||
309 | 520 | builds = [] | ||
310 | 521 | for build_instance in architectures_to_build: | ||
311 | 522 | arch = build_instance.architecture | ||
312 | 523 | try: | ||
313 | 524 | build = self.requestBuild( | ||
314 | 525 | requester, archive, supported_arches[arch], pocket, | ||
315 | 526 | channels) | ||
316 | 527 | if logger is not None: | ||
317 | 528 | logger.debug( | ||
318 | 529 | " - %s/%s/%s: Build requested.", | ||
319 | 530 | self.owner.name, self.name, arch) | ||
320 | 531 | builds.append(build) | ||
321 | 532 | except SnapBuildAlreadyPending as e: | ||
322 | 533 | if logger is not None: | ||
323 | 534 | logger.warning( | ||
324 | 535 | " - %s/%s/%s: %s", | ||
325 | 536 | self.owner.name, self.name, arch, e) | ||
326 | 537 | return builds | ||
327 | 538 | |||
328 | 498 | def requestAutoBuilds(self, allow_failures=False, logger=None): | 539 | def requestAutoBuilds(self, allow_failures=False, logger=None): |
330 | 499 | """See `ISnapSet`.""" | 540 | """See `ISnap`.""" |
331 | 500 | builds = [] | 541 | builds = [] |
332 | 501 | if self.auto_build_archive is None: | 542 | if self.auto_build_archive is None: |
333 | 502 | raise CannotRequestAutoBuilds("auto_build_archive") | 543 | raise CannotRequestAutoBuilds("auto_build_archive") |
334 | 503 | 544 | ||
335 | === added file 'lib/lp/snappy/model/snapjob.py' | |||
336 | --- lib/lp/snappy/model/snapjob.py 1970-01-01 00:00:00 +0000 | |||
337 | +++ lib/lp/snappy/model/snapjob.py 2018-06-15 13:00:33 +0000 | |||
338 | @@ -0,0 +1,271 @@ | |||
339 | 1 | # Copyright 2018 Canonical Ltd. This software is licensed under the | ||
340 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
341 | 3 | |||
342 | 4 | """Snap package jobs.""" | ||
343 | 5 | |||
344 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
345 | 7 | |||
346 | 8 | __metaclass__ = type | ||
347 | 9 | __all__ = [ | ||
348 | 10 | 'SnapJob', | ||
349 | 11 | 'SnapJobType', | ||
350 | 12 | 'SnapRequestBuildsJob', | ||
351 | 13 | ] | ||
352 | 14 | |||
353 | 15 | from lazr.delegates import delegate_to | ||
354 | 16 | from lazr.enum import ( | ||
355 | 17 | DBEnumeratedType, | ||
356 | 18 | DBItem, | ||
357 | 19 | ) | ||
358 | 20 | from storm.locals import ( | ||
359 | 21 | Int, | ||
360 | 22 | JSON, | ||
361 | 23 | Reference, | ||
362 | 24 | ) | ||
363 | 25 | from storm.store import EmptyResultSet | ||
364 | 26 | import transaction | ||
365 | 27 | from zope.component import getUtility | ||
366 | 28 | from zope.interface import ( | ||
367 | 29 | implementer, | ||
368 | 30 | provider, | ||
369 | 31 | ) | ||
370 | 32 | |||
371 | 33 | from lp.app.errors import NotFoundError | ||
372 | 34 | from lp.registry.interfaces.person import IPersonSet | ||
373 | 35 | from lp.registry.interfaces.pocket import PackagePublishingPocket | ||
374 | 36 | from lp.services.config import config | ||
375 | 37 | from lp.services.database.enumcol import EnumCol | ||
376 | 38 | from lp.services.database.interfaces import ( | ||
377 | 39 | IMasterStore, | ||
378 | 40 | IStore, | ||
379 | 41 | ) | ||
380 | 42 | from lp.services.database.stormbase import StormBase | ||
381 | 43 | from lp.services.job.model.job import ( | ||
382 | 44 | EnumeratedSubclass, | ||
383 | 45 | Job, | ||
384 | 46 | ) | ||
385 | 47 | from lp.services.job.runner import BaseRunnableJob | ||
386 | 48 | from lp.services.mail.sendmail import format_address_for_person | ||
387 | 49 | from lp.services.propertycache import cachedproperty | ||
388 | 50 | from lp.services.scripts import log | ||
389 | 51 | from lp.snappy.interfaces.snap import ( | ||
390 | 52 | CannotFetchSnapcraftYaml, | ||
391 | 53 | CannotParseSnapcraftYaml, | ||
392 | 54 | ) | ||
393 | 55 | from lp.snappy.interfaces.snapjob import ( | ||
394 | 56 | ISnapJob, | ||
395 | 57 | ISnapRequestBuildsJob, | ||
396 | 58 | ISnapRequestBuildsJobSource, | ||
397 | 59 | ) | ||
398 | 60 | from lp.snappy.model.snapbuild import SnapBuild | ||
399 | 61 | from lp.soyuz.model.archive import Archive | ||
400 | 62 | |||
401 | 63 | |||
402 | 64 | class SnapJobType(DBEnumeratedType): | ||
403 | 65 | """Values that `ISnapJob.job_type` can take.""" | ||
404 | 66 | |||
405 | 67 | REQUEST_BUILDS = DBItem(0, """ | ||
406 | 68 | Request builds | ||
407 | 69 | |||
408 | 70 | This job requests builds of a snap package. | ||
409 | 71 | """) | ||
410 | 72 | |||
411 | 73 | |||
412 | 74 | @implementer(ISnapJob) | ||
413 | 75 | class SnapJob(StormBase): | ||
414 | 76 | """See `ISnapJob`.""" | ||
415 | 77 | |||
416 | 78 | __storm_table__ = 'SnapJob' | ||
417 | 79 | |||
418 | 80 | job_id = Int(name='job', primary=True, allow_none=False) | ||
419 | 81 | job = Reference(job_id, 'Job.id') | ||
420 | 82 | |||
421 | 83 | snap_id = Int(name='snap', allow_none=False) | ||
422 | 84 | snap = Reference(snap_id, 'Snap.id') | ||
423 | 85 | |||
424 | 86 | job_type = EnumCol(enum=SnapJobType, notNull=True) | ||
425 | 87 | |||
426 | 88 | metadata = JSON('json_data', allow_none=False) | ||
427 | 89 | |||
428 | 90 | def __init__(self, snap, job_type, metadata, **job_args): | ||
429 | 91 | """Constructor. | ||
430 | 92 | |||
431 | 93 | Extra keyword arguments are used to construct the underlying Job | ||
432 | 94 | object. | ||
433 | 95 | |||
434 | 96 | :param snap: The `ISnap` this job relates to. | ||
435 | 97 | :param job_type: The `SnapJobType` of this job. | ||
436 | 98 | :param metadata: The type-specific variables, as a JSON-compatible | ||
437 | 99 | dict. | ||
438 | 100 | """ | ||
439 | 101 | super(SnapJob, self).__init__() | ||
440 | 102 | self.job = Job(**job_args) | ||
441 | 103 | self.snap = snap | ||
442 | 104 | self.job_type = job_type | ||
443 | 105 | self.metadata = metadata | ||
444 | 106 | |||
445 | 107 | def makeDerived(self): | ||
446 | 108 | return SnapJobDerived.makeSubclass(self) | ||
447 | 109 | |||
448 | 110 | |||
449 | 111 | @delegate_to(ISnapJob) | ||
450 | 112 | class SnapJobDerived(BaseRunnableJob): | ||
451 | 113 | |||
452 | 114 | __metaclass__ = EnumeratedSubclass | ||
453 | 115 | |||
454 | 116 | def __init__(self, snap_job): | ||
455 | 117 | self.context = snap_job | ||
456 | 118 | |||
457 | 119 | def __repr__(self): | ||
458 | 120 | """An informative representation of the job.""" | ||
459 | 121 | return "<%s for ~%s/+snap/%s>" % ( | ||
460 | 122 | self.__class__.__name__, self.snap.owner.name, self.snap.name) | ||
461 | 123 | |||
462 | 124 | @classmethod | ||
463 | 125 | def get(cls, job_id): | ||
464 | 126 | """Get a job by id. | ||
465 | 127 | |||
466 | 128 | :return: The `SnapJob` with the specified id, as the current | ||
467 | 129 | `SnapJobDerived` subclass. | ||
468 | 130 | :raises: `NotFoundError` if there is no job with the specified id, | ||
469 | 131 | or its `job_type` does not match the desired subclass. | ||
470 | 132 | """ | ||
471 | 133 | snap_job = IStore(SnapJob).get(SnapJob, job_id) | ||
472 | 134 | if snap_job.job_type != cls.class_job_type: | ||
473 | 135 | raise NotFoundError( | ||
474 | 136 | "No object found with id %d and type %s" % | ||
475 | 137 | (job_id, cls.class_job_type.title)) | ||
476 | 138 | return cls(snap_job) | ||
477 | 139 | |||
478 | 140 | @classmethod | ||
479 | 141 | def iterReady(cls): | ||
480 | 142 | """See `IJobSource`.""" | ||
481 | 143 | jobs = IMasterStore(SnapJob).find( | ||
482 | 144 | SnapJob, | ||
483 | 145 | SnapJob.job_type == cls.class_job_type, | ||
484 | 146 | SnapJob.job == Job.id, | ||
485 | 147 | Job.id.is_in(Job.ready_jobs)) | ||
486 | 148 | return (cls(job) for job in jobs) | ||
487 | 149 | |||
488 | 150 | def getOopsVars(self): | ||
489 | 151 | """See `IRunnableJob`.""" | ||
490 | 152 | oops_vars = super(SnapJobDerived, self).getOopsVars() | ||
491 | 153 | oops_vars.extend([ | ||
492 | 154 | ("job_id", self.context.job.id), | ||
493 | 155 | ("job_type", self.context.job_type.title), | ||
494 | 156 | ("snap_owner_name", self.context.snap.owner.name), | ||
495 | 157 | ("snap_name", self.context.snap.name), | ||
496 | 158 | ]) | ||
497 | 159 | return oops_vars | ||
498 | 160 | |||
499 | 161 | |||
500 | 162 | @implementer(ISnapRequestBuildsJob) | ||
501 | 163 | @provider(ISnapRequestBuildsJobSource) | ||
502 | 164 | class SnapRequestBuildsJob(SnapJobDerived): | ||
503 | 165 | """A Job that processes a request for builds of a snap package.""" | ||
504 | 166 | |||
505 | 167 | class_job_type = SnapJobType.REQUEST_BUILDS | ||
506 | 168 | |||
507 | 169 | user_error_types = (CannotParseSnapcraftYaml, NotFoundError) | ||
508 | 170 | retry_error_types = (CannotFetchSnapcraftYaml,) | ||
509 | 171 | |||
510 | 172 | max_retries = 5 | ||
511 | 173 | |||
512 | 174 | config = config.ISnapRequestBuildsJobSource | ||
513 | 175 | |||
514 | 176 | @classmethod | ||
515 | 177 | def create(cls, snap, requester, archive, pocket, channels): | ||
516 | 178 | """See `ISnapRequestBuildsJobSource`.""" | ||
517 | 179 | metadata = { | ||
518 | 180 | "requester": requester.id, | ||
519 | 181 | "archive": archive.id, | ||
520 | 182 | "pocket": pocket.value, | ||
521 | 183 | "channels": channels, | ||
522 | 184 | } | ||
523 | 185 | snap_job = SnapJob(snap, cls.class_job_type, metadata) | ||
524 | 186 | job = cls(snap_job) | ||
525 | 187 | job.celeryRunOnCommit() | ||
526 | 188 | return job | ||
527 | 189 | |||
528 | 190 | def getOperationDescription(self): | ||
529 | 191 | return "requesting builds of %s" % self.snap.name | ||
530 | 192 | |||
531 | 193 | def getErrorRecipients(self): | ||
532 | 194 | if self.requester is None or self.requester.preferredemail is None: | ||
533 | 195 | return [] | ||
534 | 196 | return [format_address_for_person(self.requester)] | ||
535 | 197 | |||
536 | 198 | @cachedproperty | ||
537 | 199 | def requester(self): | ||
538 | 200 | """See `ISnapRequestBuildsJob`.""" | ||
539 | 201 | requester_id = self.metadata["requester"] | ||
540 | 202 | return getUtility(IPersonSet).get(requester_id) | ||
541 | 203 | |||
542 | 204 | @cachedproperty | ||
543 | 205 | def archive(self): | ||
544 | 206 | """See `ISnapRequestBuildsJob`.""" | ||
545 | 207 | archive_id = self.metadata["archive"] | ||
546 | 208 | return IStore(Archive).find(Archive, Archive.id == archive_id).one() | ||
547 | 209 | |||
548 | 210 | @property | ||
549 | 211 | def pocket(self): | ||
550 | 212 | """See `ISnapRequestBuildsJob`.""" | ||
551 | 213 | name = self.metadata["pocket"] | ||
552 | 214 | return PackagePublishingPocket.items[name] | ||
553 | 215 | |||
554 | 216 | @property | ||
555 | 217 | def channels(self): | ||
556 | 218 | """See `ISnapRequestBuildsJob`.""" | ||
557 | 219 | return self.metadata["channels"] | ||
558 | 220 | |||
559 | 221 | @property | ||
560 | 222 | def error_message(self): | ||
561 | 223 | """See `ISnapRequestBuildsJob`.""" | ||
562 | 224 | return self.metadata.get("error_message") | ||
563 | 225 | |||
564 | 226 | @error_message.setter | ||
565 | 227 | def error_message(self, message): | ||
566 | 228 | """See `ISnapRequestBuildsJob`.""" | ||
567 | 229 | self.metadata["error_message"] = message | ||
568 | 230 | |||
569 | 231 | @property | ||
570 | 232 | def builds(self): | ||
571 | 233 | """See `ISnapRequestBuildsJob`.""" | ||
572 | 234 | build_ids = self.metadata.get("builds") | ||
573 | 235 | if build_ids is None: | ||
574 | 236 | return EmptyResultSet() | ||
575 | 237 | else: | ||
576 | 238 | return IStore(SnapBuild).find( | ||
577 | 239 | SnapBuild, SnapBuild.id.is_in(build_ids)) | ||
578 | 240 | |||
579 | 241 | @builds.setter | ||
580 | 242 | def builds(self, builds): | ||
581 | 243 | """See `ISnapRequestBuildsJob`.""" | ||
582 | 244 | self.metadata["builds"] = [build.id for build in builds] | ||
583 | 245 | |||
584 | 246 | def run(self): | ||
585 | 247 | """See `IRunnableJob`.""" | ||
586 | 248 | requester = self.requester | ||
587 | 249 | if requester is None: | ||
588 | 250 | log.info( | ||
589 | 251 | "Skipping %r because the requester has been deleted." % self) | ||
590 | 252 | return | ||
591 | 253 | archive = self.archive | ||
592 | 254 | if archive is None: | ||
593 | 255 | log.info( | ||
594 | 256 | "Skipping %r because the archive has been deleted." % self) | ||
595 | 257 | return | ||
596 | 258 | try: | ||
597 | 259 | self.builds = self.snap.requestBuildsFromJob( | ||
598 | 260 | requester, archive, self.pocket, channels=self.channels, | ||
599 | 261 | logger=log) | ||
600 | 262 | self.error_message = None | ||
601 | 263 | except self.retry_error_types: | ||
602 | 264 | raise | ||
603 | 265 | except Exception as e: | ||
604 | 266 | self.error_message = str(e) | ||
605 | 267 | # The normal job infrastructure will abort the transaction, but | ||
606 | 268 | # we want to commit instead: the only database changes we make | ||
607 | 269 | # are to this job's metadata and should be preserved. | ||
608 | 270 | transaction.commit() | ||
609 | 271 | raise | ||
610 | 0 | 272 | ||
611 | === modified file 'lib/lp/snappy/tests/test_snap.py' | |||
612 | --- lib/lp/snappy/tests/test_snap.py 2018-06-15 13:00:33 +0000 | |||
613 | +++ lib/lp/snappy/tests/test_snap.py 2018-06-15 13:00:33 +0000 | |||
614 | @@ -12,6 +12,7 @@ | |||
615 | 12 | timedelta, | 12 | timedelta, |
616 | 13 | ) | 13 | ) |
617 | 14 | import json | 14 | import json |
618 | 15 | from textwrap import dedent | ||
619 | 15 | from urlparse import urlsplit | 16 | from urlparse import urlsplit |
620 | 16 | 17 | ||
621 | 17 | from fixtures import MockPatch | 18 | from fixtures import MockPatch |
622 | @@ -91,6 +92,7 @@ | |||
623 | 91 | ISnapBuildSet, | 92 | ISnapBuildSet, |
624 | 92 | ) | 93 | ) |
625 | 93 | from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource | 94 | from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource |
626 | 95 | from lp.snappy.interfaces.snapjob import ISnapRequestBuildsJobSource | ||
627 | 94 | from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient | 96 | from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient |
628 | 95 | from lp.snappy.model.snap import SnapSet | 97 | from lp.snappy.model.snap import SnapSet |
629 | 96 | from lp.snappy.model.snapbuild import SnapFile | 98 | from lp.snappy.model.snapbuild import SnapFile |
630 | @@ -361,6 +363,65 @@ | |||
631 | 361 | snap.owner, snap.distro_series.main_archive, distroarchseries, | 363 | snap.owner, snap.distro_series.main_archive, distroarchseries, |
632 | 362 | PackagePublishingPocket.UPDATES) | 364 | PackagePublishingPocket.UPDATES) |
633 | 363 | 365 | ||
634 | 366 | def makeRequestBuildsJob(self, arch_tags): | ||
635 | 367 | distro = self.factory.makeDistribution() | ||
636 | 368 | distroseries = self.factory.makeDistroSeries(distribution=distro) | ||
637 | 369 | processors = [ | ||
638 | 370 | self.factory.makeProcessor( | ||
639 | 371 | name=arch_tag, supports_virtualized=True) | ||
640 | 372 | for arch_tag in arch_tags] | ||
641 | 373 | for processor in processors: | ||
642 | 374 | das = self.factory.makeDistroArchSeries( | ||
643 | 375 | distroseries=distroseries, architecturetag=processor.name, | ||
644 | 376 | processor=processor) | ||
645 | 377 | das.addOrUpdateChroot(self.factory.makeLibraryFileAlias( | ||
646 | 378 | filename="fake_chroot.tar.gz", db_only=True)) | ||
647 | 379 | [git_ref] = self.factory.makeGitRefs() | ||
648 | 380 | snap = self.factory.makeSnap( | ||
649 | 381 | git_ref=git_ref, distroseries=distroseries, processors=processors) | ||
650 | 382 | return getUtility(ISnapRequestBuildsJobSource).create( | ||
651 | 383 | snap, snap.owner.teamowner, distro.main_archive, | ||
652 | 384 | PackagePublishingPocket.RELEASE, {"snapcraft": "edge"}) | ||
653 | 385 | |||
654 | 386 | def assertRequestedBuildsMatch(self, builds, job, arch_tags): | ||
655 | 387 | self.assertThat(builds, MatchesSetwise( | ||
656 | 388 | *(MatchesStructure( | ||
657 | 389 | requester=Equals(job.requester), | ||
658 | 390 | snap=Equals(job.snap), | ||
659 | 391 | archive=Equals(job.archive), | ||
660 | 392 | distro_arch_series=Equals(job.snap.distro_series[arch_tag]), | ||
661 | 393 | pocket=Equals(job.pocket), | ||
662 | 394 | channels=Equals(job.channels)) | ||
663 | 395 | for arch_tag in arch_tags))) | ||
664 | 396 | |||
665 | 397 | def test_requestBuildsFromJob_restricts_explicit_list(self): | ||
666 | 398 | # requestBuildsFromJob limits builds targeted at an explicit list of | ||
667 | 399 | # architectures to those allowed for the snap. | ||
668 | 400 | self.useFixture(GitHostingFixture(blob=dedent("""\ | ||
669 | 401 | architectures: | ||
670 | 402 | - build-on: sparc | ||
671 | 403 | - build-on: i386 | ||
672 | 404 | - build-on: avr | ||
673 | 405 | """))) | ||
674 | 406 | job = self.makeRequestBuildsJob(["sparc"]) | ||
675 | 407 | with person_logged_in(job.requester): | ||
676 | 408 | builds = job.snap.requestBuildsFromJob( | ||
677 | 409 | job.requester, job.archive, job.pocket, | ||
678 | 410 | removeSecurityProxy(job.channels)) | ||
679 | 411 | self.assertRequestedBuildsMatch(builds, job, ["sparc"]) | ||
680 | 412 | |||
681 | 413 | def test_requestBuildsFromJob_no_explicit_architectures(self): | ||
682 | 414 | # If the snap doesn't specify any architectures, | ||
683 | 415 | # requestBuildsFromJob requests builds for all configured | ||
684 | 416 | # architectures. | ||
685 | 417 | self.useFixture(GitHostingFixture(blob="name: foo\n")) | ||
686 | 418 | job = self.makeRequestBuildsJob(["mips64el", "riscv64"]) | ||
687 | 419 | with person_logged_in(job.requester): | ||
688 | 420 | builds = job.snap.requestBuildsFromJob( | ||
689 | 421 | job.requester, job.archive, job.pocket, | ||
690 | 422 | removeSecurityProxy(job.channels)) | ||
691 | 423 | self.assertRequestedBuildsMatch(builds, job, ["mips64el", "riscv64"]) | ||
692 | 424 | |||
693 | 364 | def test_requestAutoBuilds(self): | 425 | def test_requestAutoBuilds(self): |
694 | 365 | # requestAutoBuilds creates new builds for all configured | 426 | # requestAutoBuilds creates new builds for all configured |
695 | 366 | # architectures with appropriate parameters. | 427 | # architectures with appropriate parameters. |
696 | 367 | 428 | ||
697 | === added file 'lib/lp/snappy/tests/test_snapjob.py' | |||
698 | --- lib/lp/snappy/tests/test_snapjob.py 1970-01-01 00:00:00 +0000 | |||
699 | +++ lib/lp/snappy/tests/test_snapjob.py 2018-06-15 13:00:33 +0000 | |||
700 | @@ -0,0 +1,155 @@ | |||
701 | 1 | # Copyright 2018 Canonical Ltd. This software is licensed under the | ||
702 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
703 | 3 | |||
704 | 4 | """Tests for snap package jobs.""" | ||
705 | 5 | |||
706 | 6 | from __future__ import absolute_import, print_function, unicode_literals | ||
707 | 7 | |||
708 | 8 | __metaclass__ = type | ||
709 | 9 | |||
710 | 10 | from textwrap import dedent | ||
711 | 11 | |||
712 | 12 | from testtools.matchers import ( | ||
713 | 13 | AfterPreprocessing, | ||
714 | 14 | ContainsDict, | ||
715 | 15 | Equals, | ||
716 | 16 | Is, | ||
717 | 17 | MatchesSetwise, | ||
718 | 18 | MatchesStructure, | ||
719 | 19 | ) | ||
720 | 20 | |||
721 | 21 | from lp.code.tests.helpers import GitHostingFixture | ||
722 | 22 | from lp.registry.interfaces.pocket import PackagePublishingPocket | ||
723 | 23 | from lp.services.config import config | ||
724 | 24 | from lp.services.job.interfaces.job import JobStatus | ||
725 | 25 | from lp.services.job.runner import JobRunner | ||
726 | 26 | from lp.services.mail.sendmail import format_address_for_person | ||
727 | 27 | from lp.snappy.interfaces.snap import CannotParseSnapcraftYaml | ||
728 | 28 | from lp.snappy.interfaces.snapjob import ( | ||
729 | 29 | ISnapJob, | ||
730 | 30 | ISnapRequestBuildsJob, | ||
731 | 31 | ) | ||
732 | 32 | from lp.snappy.model.snapjob import ( | ||
733 | 33 | SnapJob, | ||
734 | 34 | SnapJobType, | ||
735 | 35 | SnapRequestBuildsJob, | ||
736 | 36 | ) | ||
737 | 37 | from lp.testing import TestCaseWithFactory | ||
738 | 38 | from lp.testing.dbuser import dbuser | ||
739 | 39 | from lp.testing.layers import ZopelessDatabaseLayer | ||
740 | 40 | |||
741 | 41 | |||
742 | 42 | class TestSnapJob(TestCaseWithFactory): | ||
743 | 43 | |||
744 | 44 | layer = ZopelessDatabaseLayer | ||
745 | 45 | |||
746 | 46 | def test_provides_interface(self): | ||
747 | 47 | # `SnapJob` objects provide `ISnapJob`. | ||
748 | 48 | snap = self.factory.makeSnap() | ||
749 | 49 | self.assertProvides( | ||
750 | 50 | SnapJob(snap, SnapJobType.REQUEST_BUILDS, {}), ISnapJob) | ||
751 | 51 | |||
752 | 52 | |||
753 | 53 | class TestSnapRequestBuildsJob(TestCaseWithFactory): | ||
754 | 54 | |||
755 | 55 | layer = ZopelessDatabaseLayer | ||
756 | 56 | |||
757 | 57 | def test_provides_interface(self): | ||
758 | 58 | # `SnapRequestBuildsJob` objects provide `ISnapRequestBuildsJob`.""" | ||
759 | 59 | snap = self.factory.makeSnap() | ||
760 | 60 | archive = self.factory.makeArchive() | ||
761 | 61 | job = SnapRequestBuildsJob.create( | ||
762 | 62 | snap, snap.registrant, archive, PackagePublishingPocket.RELEASE, | ||
763 | 63 | None) | ||
764 | 64 | self.assertProvides(job, ISnapRequestBuildsJob) | ||
765 | 65 | |||
766 | 66 | def test___repr__(self): | ||
767 | 67 | # `SnapRequestBuildsJob` objects have an informative __repr__. | ||
768 | 68 | snap = self.factory.makeSnap() | ||
769 | 69 | archive = self.factory.makeArchive() | ||
770 | 70 | job = SnapRequestBuildsJob.create( | ||
771 | 71 | snap, snap.registrant, archive, PackagePublishingPocket.RELEASE, | ||
772 | 72 | None) | ||
773 | 73 | self.assertEqual( | ||
774 | 74 | "<SnapRequestBuildsJob for ~%s/+snap/%s>" % ( | ||
775 | 75 | snap.owner.name, snap.name), | ||
776 | 76 | repr(job)) | ||
777 | 77 | |||
778 | 78 | def makeSeriesAndProcessors(self, arch_tags): | ||
779 | 79 | distro = self.factory.makeDistribution() | ||
780 | 80 | distroseries = self.factory.makeDistroSeries(distribution=distro) | ||
781 | 81 | processors = [ | ||
782 | 82 | self.factory.makeProcessor( | ||
783 | 83 | name=arch_tag, supports_virtualized=True) | ||
784 | 84 | for arch_tag in arch_tags] | ||
785 | 85 | for processor in processors: | ||
786 | 86 | das = self.factory.makeDistroArchSeries( | ||
787 | 87 | distroseries=distroseries, architecturetag=processor.name, | ||
788 | 88 | processor=processor) | ||
789 | 89 | das.addOrUpdateChroot(self.factory.makeLibraryFileAlias( | ||
790 | 90 | filename="fake_chroot.tar.gz", db_only=True)) | ||
791 | 91 | return distroseries, processors | ||
792 | 92 | |||
793 | 93 | def test_run(self): | ||
794 | 94 | # The job requests builds and records the result. | ||
795 | 95 | distroseries, processors = self.makeSeriesAndProcessors( | ||
796 | 96 | ["avr2001", "sparc64", "x32"]) | ||
797 | 97 | [git_ref] = self.factory.makeGitRefs() | ||
798 | 98 | snap = self.factory.makeSnap( | ||
799 | 99 | git_ref=git_ref, distroseries=distroseries, processors=processors) | ||
800 | 100 | job = SnapRequestBuildsJob.create( | ||
801 | 101 | snap, snap.registrant, distroseries.main_archive, | ||
802 | 102 | PackagePublishingPocket.RELEASE, {"core": "stable"}) | ||
803 | 103 | snapcraft_yaml = dedent("""\ | ||
804 | 104 | architectures: | ||
805 | 105 | - build-on: avr2001 | ||
806 | 106 | - build-on: x32 | ||
807 | 107 | """) | ||
808 | 108 | self.useFixture(GitHostingFixture(blob=snapcraft_yaml)) | ||
809 | 109 | with dbuser(config.ISnapRequestBuildsJobSource.dbuser): | ||
810 | 110 | JobRunner([job]).runAll() | ||
811 | 111 | self.assertEmailQueueLength(0) | ||
812 | 112 | self.assertThat(job, MatchesStructure( | ||
813 | 113 | job=MatchesStructure.byEquality(status=JobStatus.COMPLETED), | ||
814 | 114 | error_message=Is(None), | ||
815 | 115 | builds=AfterPreprocessing(set, MatchesSetwise(*[ | ||
816 | 116 | MatchesStructure.byEquality( | ||
817 | 117 | requester=snap.registrant, | ||
818 | 118 | snap=snap, | ||
819 | 119 | archive=distroseries.main_archive, | ||
820 | 120 | distro_arch_series=distroseries[arch], | ||
821 | 121 | pocket=PackagePublishingPocket.RELEASE, | ||
822 | 122 | channels={"core": "stable"}) | ||
823 | 123 | for arch in ("avr2001", "x32")])))) | ||
824 | 124 | |||
825 | 125 | def test_run_failed(self): | ||
826 | 126 | # A failed run sets the job status to FAILED and records the error | ||
827 | 127 | # message. | ||
828 | 128 | # The job requests builds and records the result. | ||
829 | 129 | distroseries, processors = self.makeSeriesAndProcessors( | ||
830 | 130 | ["avr2001", "sparc64", "x32"]) | ||
831 | 131 | [git_ref] = self.factory.makeGitRefs() | ||
832 | 132 | snap = self.factory.makeSnap( | ||
833 | 133 | git_ref=git_ref, distroseries=distroseries, processors=processors) | ||
834 | 134 | job = SnapRequestBuildsJob.create( | ||
835 | 135 | snap, snap.registrant, distroseries.main_archive, | ||
836 | 136 | PackagePublishingPocket.RELEASE, {"core": "stable"}) | ||
837 | 137 | self.useFixture(GitHostingFixture()).getBlob.failure = ( | ||
838 | 138 | CannotParseSnapcraftYaml("Nonsense on stilts")) | ||
839 | 139 | with dbuser(config.ISnapRequestBuildsJobSource.dbuser): | ||
840 | 140 | JobRunner([job]).runAll() | ||
841 | 141 | [notification] = self.assertEmailQueueLength(1) | ||
842 | 142 | self.assertThat(dict(notification), ContainsDict({ | ||
843 | 143 | "From": Equals(config.canonical.noreply_from_address), | ||
844 | 144 | "To": Equals(format_address_for_person(snap.registrant)), | ||
845 | 145 | "Subject": Equals( | ||
846 | 146 | "Launchpad error while requesting builds of %s" % snap.name), | ||
847 | 147 | })) | ||
848 | 148 | self.assertEqual( | ||
849 | 149 | "Launchpad encountered an error during the following operation: " | ||
850 | 150 | "requesting builds of %s. Nonsense on stilts" % snap.name, | ||
851 | 151 | notification.get_payload(decode=True)) | ||
852 | 152 | self.assertThat(job, MatchesStructure( | ||
853 | 153 | job=MatchesStructure.byEquality(status=JobStatus.FAILED), | ||
854 | 154 | error_message=Equals("Nonsense on stilts"), | ||
855 | 155 | builds=AfterPreprocessing(set, MatchesSetwise()))) |