Merge lp:~cjwatson/launchpad/snap-request-build-explicit-arch-consistency into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18996
Proposed branch: lp:~cjwatson/launchpad/snap-request-build-explicit-arch-consistency
Merge into: lp:launchpad
Diff against target: 600 lines (+201/-114)
8 files modified
lib/lp/snappy/browser/snap.py (+8/-51)
lib/lp/snappy/browser/tests/test_snap.py (+34/-39)
lib/lp/snappy/interfaces/snap.py (+11/-2)
lib/lp/snappy/interfaces/snapjob.py (+9/-1)
lib/lp/snappy/model/snap.py (+14/-4)
lib/lp/snappy/model/snapjob.py (+14/-2)
lib/lp/snappy/tests/test_snap.py (+70/-14)
lib/lp/snappy/tests/test_snapjob.py (+41/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-request-build-explicit-arch-consistency
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+369162@code.launchpad.net

Commit message

Use build request jobs for all snap build requests in the web UI.

Description of the change

Previously build request jobs were only used when people didn't explicitly select architectures, which was very confusing since it meant bases weren't honoured properly.

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 'lib/lp/snappy/browser/snap.py'
2--- lib/lp/snappy/browser/snap.py 2019-05-16 10:21:14 +0000
3+++ lib/lp/snappy/browser/snap.py 2019-06-21 11:37:21 +0000
4@@ -57,7 +57,6 @@
5 from lp.registry.enums import VCSType
6 from lp.registry.interfaces.pocket import PackagePublishingPocket
7 from lp.services.features import getFeatureFlag
8-from lp.services.helpers import english_list
9 from lp.services.propertycache import cachedproperty
10 from lp.services.scripts import log
11 from lp.services.utils import seconds_since_epoch
12@@ -92,7 +91,6 @@
13 MissingSnapcraftYaml,
14 NoSuchSnap,
15 SNAP_PRIVATE_FEATURE_FLAG,
16- SnapBuildAlreadyPending,
17 SnapPrivateFeatureDisabled,
18 )
19 from lp.snappy.interfaces.snapbuild import ISnapBuildSet
20@@ -256,20 +254,6 @@
21 return items
22
23
24-def new_builds_notification_text(builds, already_pending=None):
25- nr_builds = len(builds)
26- if not nr_builds:
27- builds_text = "All requested builds are already queued."
28- elif nr_builds == 1:
29- builds_text = "1 new build has been queued."
30- else:
31- builds_text = "%d new builds have been queued." % nr_builds
32- if nr_builds and already_pending:
33- return structured("<p>%s</p><p>%s</p>", builds_text, already_pending)
34- else:
35- return builds_text
36-
37-
38 class SnapRequestBuildsView(LaunchpadFormView):
39 """A view for requesting builds of a snap package."""
40
41@@ -328,45 +312,18 @@
42 'channels': self.context.auto_build_channels,
43 }
44
45- def requestBuild(self, data):
46- """User action for requesting a number of builds.
47-
48- We raise exceptions for most errors, but if there's already a
49- pending build for a particular architecture, we simply record that
50- so that other builds can be queued and a message displayed to the
51- caller.
52- """
53- informational = {}
54- builds = []
55- already_pending = []
56- for arch in data['distro_arch_series']:
57- try:
58- build = self.context.requestBuild(
59- self.user, data['archive'], arch, data['pocket'],
60- channels=data['channels'])
61- builds.append(build)
62- except SnapBuildAlreadyPending:
63- already_pending.append(arch)
64- if already_pending:
65- informational['already_pending'] = (
66- "An identical build is already pending for %s." %
67- english_list(arch.architecturetag for arch in already_pending))
68- return builds, informational
69-
70 @action('Request builds', name='request')
71 def request_action(self, action, data):
72 if data.get('distro_arch_series', []):
73- builds, informational = self.requestBuild(data)
74- already_pending = informational.get('already_pending')
75- notification_text = new_builds_notification_text(
76- builds, already_pending)
77- self.request.response.addNotification(notification_text)
78+ architectures = [
79+ arch.architecturetag for arch in data['distro_arch_series']]
80 else:
81- self.context.requestBuilds(
82- self.user, data['archive'], data['pocket'],
83- channels=data['channels'])
84- self.request.response.addNotification(
85- _('Builds will be dispatched soon.'))
86+ architectures = None
87+ self.context.requestBuilds(
88+ self.user, data['archive'], data['pocket'],
89+ architectures=architectures, channels=data['channels'])
90+ self.request.response.addNotification(
91+ _('Builds will be dispatched soon.'))
92 self.next_url = self.cancel_url
93
94
95
96=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
97--- lib/lp/snappy/browser/tests/test_snap.py 2019-06-11 09:39:29 +0000
98+++ lib/lp/snappy/browser/tests/test_snap.py 2019-06-21 11:37:21 +0000
99@@ -29,6 +29,7 @@
100 AfterPreprocessing,
101 Equals,
102 Is,
103+ MatchesDict,
104 MatchesListwise,
105 MatchesSetwise,
106 MatchesStructure,
107@@ -1690,8 +1691,8 @@
108 Unauthorized, self.getViewBrowser, self.snap, "+request-builds")
109
110 def test_request_builds_with_architectures_action(self):
111- # Requesting a build with architectures selected creates pending
112- # builds.
113+ # Requesting a build with architectures selected creates a pending
114+ # build request limited to those architectures.
115 browser = self.getViewBrowser(
116 self.snap, "+request-builds", user=self.person)
117 browser.getControl("amd64").selected = True
118@@ -1699,21 +1700,24 @@
119 browser.getControl("Request builds").click()
120
121 login_person(self.person)
122- builds = self.snap.pending_builds
123- self.assertContentEqual(
124- [self.ubuntu.main_archive], set(build.archive for build in builds))
125- self.assertContentEqual(
126- ["amd64", "i386"],
127- [build.distro_arch_series.architecturetag for build in builds])
128- self.assertContentEqual(
129- [PackagePublishingPocket.UPDATES],
130- set(build.pocket for build in builds))
131- self.assertContentEqual(
132- [2510], set(build.buildqueue_record.lastscore for build in builds))
133+ [request] = self.snap.pending_build_requests
134+ self.assertThat(removeSecurityProxy(request), MatchesStructure(
135+ snap=Equals(self.snap),
136+ status=Equals(SnapBuildRequestStatus.PENDING),
137+ error_message=Is(None),
138+ builds=AfterPreprocessing(list, Equals([])),
139+ archive=Equals(self.ubuntu.main_archive),
140+ _job=MatchesStructure(
141+ requester=Equals(self.person),
142+ pocket=Equals(PackagePublishingPocket.UPDATES),
143+ channels=Equals({}),
144+ architectures=MatchesSetwise(Equals("amd64"), Equals("i386")),
145+ )))
146
147 def test_request_builds_with_architectures_ppa(self):
148 # Selecting a different archive with architectures selected creates
149- # builds in that archive.
150+ # a build request targeting that archive and limited to those
151+ # architectures.
152 ppa = self.factory.makeArchive(
153 distribution=self.ubuntu, owner=self.person, name="snap-ppa")
154 browser = self.getViewBrowser(
155@@ -1725,12 +1729,15 @@
156 browser.getControl("Request builds").click()
157
158 login_person(self.person)
159- builds = self.snap.pending_builds
160- self.assertEqual([ppa], [build.archive for build in builds])
161+ [request] = self.snap.pending_build_requests
162+ self.assertThat(request, MatchesStructure(
163+ archive=Equals(ppa),
164+ architectures=MatchesSetwise(Equals("amd64"))))
165
166 def test_request_builds_with_architectures_channels(self):
167- # Selecting different channels with architectures selected creates
168- # builds using those channels.
169+ # Selecting different channels with architectures selected creates a
170+ # build request using those channels and limited to those
171+ # architectures.
172 browser = self.getViewBrowser(
173 self.snap, "+request-builds", user=self.person)
174 browser.getControl(name="field.channels.core").value = "edge"
175@@ -1739,25 +1746,10 @@
176 browser.getControl("Request builds").click()
177
178 login_person(self.person)
179- builds = self.snap.pending_builds
180- self.assertEqual(
181- [{"core": "edge"}], [build.channels for build in builds])
182-
183- def test_request_builds_with_architectures_rejects_duplicate(self):
184- # A duplicate build request with architectures selected causes a
185- # notification.
186- self.snap.requestBuild(
187- self.person, self.ubuntu.main_archive, self.distroseries["amd64"],
188- PackagePublishingPocket.UPDATES)
189- browser = self.getViewBrowser(
190- self.snap, "+request-builds", user=self.person)
191- browser.getControl("amd64").selected = True
192- browser.getControl("i386").selected = True
193- browser.getControl("Request builds").click()
194- main_text = extract_text(find_main_content(browser.contents))
195- self.assertIn("1 new build has been queued.", main_text)
196- self.assertIn(
197- "An identical build is already pending for amd64.", main_text)
198+ [request] = self.snap.pending_build_requests
199+ self.assertThat(request, MatchesStructure(
200+ channels=MatchesDict({"core": Equals("edge")}),
201+ architectures=MatchesSetwise(Equals("amd64"))))
202
203 def test_request_builds_no_architectures_action(self):
204 # Requesting a build with no architectures selected creates a
205@@ -1779,7 +1771,8 @@
206 _job=MatchesStructure(
207 requester=Equals(self.person),
208 pocket=Equals(PackagePublishingPocket.UPDATES),
209- channels=Equals({}))))
210+ channels=Equals({}),
211+ architectures=Is(None))))
212
213 def test_request_builds_no_architectures_ppa(self):
214 # Selecting a different archive with no architectures selected
215@@ -1796,7 +1789,9 @@
216
217 login_person(self.person)
218 [request] = self.snap.pending_build_requests
219- self.assertEqual(ppa, request.archive)
220+ self.assertThat(request, MatchesStructure(
221+ archive=Equals(ppa),
222+ architectures=Is(None)))
223
224 def test_request_builds_no_architectures_channels(self):
225 # Selecting different channels with no architectures selected
226
227=== modified file 'lib/lp/snappy/interfaces/snap.py'
228--- lib/lp/snappy/interfaces/snap.py 2019-05-22 18:33:10 +0000
229+++ lib/lp/snappy/interfaces/snap.py 2019-06-21 11:37:21 +0000
230@@ -76,6 +76,7 @@
231 Dict,
232 Int,
233 List,
234+ Set,
235 Text,
236 TextLine,
237 )
238@@ -336,6 +337,10 @@
239 title=_("Source snap channels for builds produced by this request"),
240 key_type=TextLine(), required=False, readonly=True)
241
242+ architectures = Set(
243+ title=_("If set, this request is limited to these architecture tags"),
244+ value_type=TextLine(), required=False, readonly=True)
245+
246
247 class ISnapView(Interface):
248 """`ISnap` attributes that require launchpad.View permission."""
249@@ -437,8 +442,9 @@
250 """
251
252 def requestBuildsFromJob(requester, archive, pocket, channels=None,
253- allow_failures=False, fetch_snapcraft_yaml=True,
254- build_request=None, logger=None):
255+ architectures=None, allow_failures=False,
256+ fetch_snapcraft_yaml=True, build_request=None,
257+ logger=None):
258 """Synchronous part of `Snap.requestBuilds`.
259
260 Request that the snap package be built for relevant architectures.
261@@ -448,6 +454,9 @@
262 :param pocket: The pocket that should be targeted.
263 :param channels: A dictionary mapping snap names to channels to use
264 for these builds.
265+ :param architectures: If not None, limit builds to architectures
266+ with these architecture tags (in addition to any other
267+ applicable constraints).
268 :param allow_failures: If True, log exceptions other than "already
269 pending" from individual build requests; if False, raise them to
270 the caller.
271
272=== modified file 'lib/lp/snappy/interfaces/snapjob.py'
273--- lib/lp/snappy/interfaces/snapjob.py 2019-05-22 18:33:10 +0000
274+++ lib/lp/snappy/interfaces/snapjob.py 2019-06-21 11:37:21 +0000
275@@ -22,6 +22,7 @@
276 Datetime,
277 Dict,
278 List,
279+ Set,
280 TextLine,
281 )
282
283@@ -78,6 +79,10 @@
284 "are supported."),
285 key_type=TextLine(), required=False, readonly=True)
286
287+ architectures = Set(
288+ title=_("If set, limit builds to these architecture tags."),
289+ value_type=TextLine(), required=False, readonly=True)
290+
291 date_created = Datetime(
292 title=_("Time when this job was created."),
293 required=True, readonly=True)
294@@ -101,7 +106,7 @@
295
296 class ISnapRequestBuildsJobSource(IJobSource):
297
298- def create(snap, requester, archive, pocket, channels):
299+ def create(snap, requester, archive, pocket, channels, architectures=None):
300 """Request builds of a snap package.
301
302 :param snap: The snap package to build.
303@@ -110,6 +115,9 @@
304 :param pocket: The pocket that should be targeted.
305 :param channels: A dictionary mapping snap names to channels to use
306 for these builds.
307+ :param architectures: If not None, limit builds to architectures
308+ with these architecture tags (in addition to any other
309+ applicable constraints).
310 """
311
312 def findBySnap(snap, statuses=None, job_ids=None):
313
314=== modified file 'lib/lp/snappy/model/snap.py'
315--- lib/lp/snappy/model/snap.py 2019-05-16 10:21:14 +0000
316+++ lib/lp/snappy/model/snap.py 2019-06-21 11:37:21 +0000
317@@ -249,6 +249,11 @@
318 """See `ISnapBuildRequest`."""
319 return self._job.channels
320
321+ @property
322+ def architectures(self):
323+ """See `ISnapBuildRequest`."""
324+ return self._job.architectures
325+
326
327 @implementer(ISnap, IHasOwner)
328 class Snap(Storm, WebhookTargetMixin):
329@@ -655,11 +660,13 @@
330 notify(ObjectCreatedEvent(build, user=requester))
331 return build
332
333- def requestBuilds(self, requester, archive, pocket, channels=None):
334+ def requestBuilds(self, requester, archive, pocket, channels=None,
335+ architectures=None):
336 """See `ISnap`."""
337 self._checkRequestBuild(requester, archive)
338 job = getUtility(ISnapRequestBuildsJobSource).create(
339- self, requester, archive, pocket, channels)
340+ self, requester, archive, pocket, channels,
341+ architectures=architectures)
342 return self.getBuildRequest(job.job_id)
343
344 @staticmethod
345@@ -694,7 +701,8 @@
346 else:
347 return channels
348
349- def requestBuildsFromJob(self, requester, archive, pocket, channels=None,
350+ def requestBuildsFromJob(self, requester, archive, pocket,
351+ channels=None, architectures=None,
352 allow_failures=False, fetch_snapcraft_yaml=True,
353 build_request=None, logger=None):
354 """See `ISnap`."""
355@@ -731,7 +739,9 @@
356 supported_arches = OrderedDict(
357 (das.architecturetag, das) for das in sorted(
358 self.getAllowedArchitectures(distro_series),
359- key=attrgetter("processor.id")))
360+ key=attrgetter("processor.id"))
361+ if (architectures is None or
362+ das.architecturetag in architectures))
363 architectures_to_build = determine_architectures_to_build(
364 snapcraft_data, supported_arches.keys())
365 except Exception as e:
366
367=== modified file 'lib/lp/snappy/model/snapjob.py'
368--- lib/lp/snappy/model/snapjob.py 2018-10-09 09:25:19 +0000
369+++ lib/lp/snappy/model/snapjob.py 2019-06-21 11:37:21 +0000
370@@ -1,4 +1,4 @@
371-# Copyright 2018 Canonical Ltd. This software is licensed under the
372+# Copyright 2018-2019 Canonical Ltd. This software is licensed under the
373 # GNU Affero General Public License version 3 (see the file LICENSE).
374
375 """Snap package jobs."""
376@@ -184,13 +184,18 @@
377 config = config.ISnapRequestBuildsJobSource
378
379 @classmethod
380- def create(cls, snap, requester, archive, pocket, channels):
381+ def create(cls, snap, requester, archive, pocket, channels,
382+ architectures=None):
383 """See `ISnapRequestBuildsJobSource`."""
384 metadata = {
385 "requester": requester.id,
386 "archive": archive.id,
387 "pocket": pocket.value,
388 "channels": channels,
389+ # Really a set or None, but sets aren't directly
390+ # JSON-serialisable.
391+ "architectures": (
392+ list(architectures) if architectures is not None else None),
393 }
394 snap_job = SnapJob(snap, cls.class_job_type, metadata)
395 job = cls(snap_job)
396@@ -292,6 +297,12 @@
397 return self.metadata["channels"]
398
399 @property
400+ def architectures(self):
401+ """See `ISnapRequestBuildsJob`."""
402+ architectures = self.metadata["architectures"]
403+ return set(architectures) if architectures is not None else None
404+
405+ @property
406 def date_created(self):
407 """See `ISnapRequestBuildsJob`."""
408 return self.context.job.date_created
409@@ -346,6 +357,7 @@
410 try:
411 self.builds = self.snap.requestBuildsFromJob(
412 requester, archive, self.pocket, channels=self.channels,
413+ architectures=self.architectures,
414 build_request=self.build_request, logger=log)
415 self.error_message = None
416 except self.retry_error_types:
417
418=== modified file 'lib/lp/snappy/tests/test_snap.py'
419--- lib/lp/snappy/tests/test_snap.py 2019-06-20 13:16:35 +0000
420+++ lib/lp/snappy/tests/test_snap.py 2019-06-21 11:37:21 +0000
421@@ -475,7 +475,9 @@
422 status=Equals(SnapBuildRequestStatus.PENDING),
423 error_message=Is(None),
424 builds=AfterPreprocessing(set, MatchesSetwise()),
425- archive=Equals(snap.distro_series.main_archive)))
426+ archive=Equals(snap.distro_series.main_archive),
427+ channels=MatchesDict({"snapcraft": Equals("edge")}),
428+ architectures=Is(None)))
429 [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()
430 self.assertThat(job, MatchesStructure(
431 job_id=Equals(request.id),
432@@ -484,7 +486,8 @@
433 requester=Equals(snap.owner.teamowner),
434 archive=Equals(snap.distro_series.main_archive),
435 pocket=Equals(PackagePublishingPocket.UPDATES),
436- channels=Equals({"snapcraft": "edge"})))
437+ channels=Equals({"snapcraft": "edge"}),
438+ architectures=Is(None)))
439
440 def test_requestBuilds_without_distroseries(self):
441 # requestBuilds schedules a job for a snap without a distroseries.
442@@ -502,16 +505,51 @@
443 status=Equals(SnapBuildRequestStatus.PENDING),
444 error_message=Is(None),
445 builds=AfterPreprocessing(set, MatchesSetwise()),
446- archive=Equals(archive)))
447- [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()
448- self.assertThat(job, MatchesStructure(
449- job_id=Equals(request.id),
450- job=MatchesStructure.byEquality(status=JobStatus.WAITING),
451- snap=Equals(snap),
452- requester=Equals(snap.owner.teamowner),
453- archive=Equals(archive),
454- pocket=Equals(PackagePublishingPocket.UPDATES),
455- channels=Equals({"snapcraft": "edge"})))
456+ archive=Equals(archive),
457+ channels=MatchesDict({"snapcraft": Equals("edge")}),
458+ architectures=Is(None)))
459+ [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()
460+ self.assertThat(job, MatchesStructure(
461+ job_id=Equals(request.id),
462+ job=MatchesStructure.byEquality(status=JobStatus.WAITING),
463+ snap=Equals(snap),
464+ requester=Equals(snap.owner.teamowner),
465+ archive=Equals(archive),
466+ pocket=Equals(PackagePublishingPocket.UPDATES),
467+ channels=Equals({"snapcraft": "edge"}),
468+ architectures=Is(None)))
469+
470+ def test_requestBuilds_with_architectures(self):
471+ # If asked to build for particular architectures, requestBuilds
472+ # passes those through to the job.
473+ snap = self.factory.makeSnap()
474+ now = get_transaction_timestamp(IStore(snap))
475+ with person_logged_in(snap.owner.teamowner):
476+ request = snap.requestBuilds(
477+ snap.owner.teamowner, snap.distro_series.main_archive,
478+ PackagePublishingPocket.UPDATES,
479+ channels={"snapcraft": "edge"},
480+ architectures={"amd64", "i386"})
481+ self.assertThat(request, MatchesStructure(
482+ date_requested=Equals(now),
483+ date_finished=Is(None),
484+ snap=Equals(snap),
485+ status=Equals(SnapBuildRequestStatus.PENDING),
486+ error_message=Is(None),
487+ builds=AfterPreprocessing(set, MatchesSetwise()),
488+ archive=Equals(snap.distro_series.main_archive),
489+ channels=MatchesDict({"snapcraft": Equals("edge")}),
490+ architectures=MatchesSetwise(Equals("amd64"), Equals("i386"))))
491+ [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()
492+ self.assertThat(job, MatchesStructure(
493+ job_id=Equals(request.id),
494+ job=MatchesStructure.byEquality(status=JobStatus.WAITING),
495+ snap=Equals(snap),
496+ requester=Equals(snap.owner.teamowner),
497+ archive=Equals(snap.distro_series.main_archive),
498+ pocket=Equals(PackagePublishingPocket.UPDATES),
499+ channels=Equals({"snapcraft": "edge"}),
500+ architectures=MatchesSetwise(Equals("amd64"), Equals("i386"))))
501
502 def test__findBase(self):
503 snap_base_set = getUtility(ISnapBaseSet)
504@@ -585,7 +623,7 @@
505 with person_logged_in(job.requester):
506 builds = job.snap.requestBuildsFromJob(
507 job.requester, job.archive, job.pocket,
508- removeSecurityProxy(job.channels),
509+ channels=removeSecurityProxy(job.channels),
510 build_request=job.build_request)
511 self.assertRequestedBuildsMatch(builds, job, ["sparc"], job.channels)
512
513@@ -601,11 +639,29 @@
514 with person_logged_in(job.requester):
515 builds = job.snap.requestBuildsFromJob(
516 job.requester, job.archive, job.pocket,
517- removeSecurityProxy(job.channels),
518+ channels=removeSecurityProxy(job.channels),
519 build_request=job.build_request)
520 self.assertRequestedBuildsMatch(
521 builds, job, ["mips64el", "riscv64"], job.channels)
522
523+ def test_requestBuildsFromJob_architectures_parameter(self):
524+ # If an explicit set of architectures was given as a parameter,
525+ # requestBuildsFromJob intersects those with any other constraints
526+ # when requesting builds.
527+ self.useFixture(GitHostingFixture(blob="name: foo\n"))
528+ job = self.makeRequestBuildsJob(["avr", "mips64el", "riscv64"])
529+ self.assertEqual(
530+ get_transaction_timestamp(IStore(job.snap)), job.date_created)
531+ transaction.commit()
532+ with person_logged_in(job.requester):
533+ builds = job.snap.requestBuildsFromJob(
534+ job.requester, job.archive, job.pocket,
535+ channels=removeSecurityProxy(job.channels),
536+ architectures={"avr", "riscv64"},
537+ build_request=job.build_request)
538+ self.assertRequestedBuildsMatch(
539+ builds, job, ["avr", "riscv64"], job.channels)
540+
541 def test_requestBuildsFromJob_no_distroseries_explicit_base(self):
542 # If the snap doesn't specify a distroseries but has an explicit
543 # base, requestBuildsFromJob requests builds for the appropriate
544
545=== modified file 'lib/lp/snappy/tests/test_snapjob.py'
546--- lib/lp/snappy/tests/test_snapjob.py 2018-09-10 11:18:42 +0000
547+++ lib/lp/snappy/tests/test_snapjob.py 2019-06-21 11:37:21 +0000
548@@ -1,4 +1,4 @@
549-# Copyright 2018 Canonical Ltd. This software is licensed under the
550+# Copyright 2018-2019 Canonical Ltd. This software is licensed under the
551 # GNU Affero General Public License version 3 (see the file LICENSE).
552
553 """Tests for snap package jobs."""
554@@ -133,6 +133,46 @@
555 channels=Equals({"core": "stable"}))
556 for arch in ("avr2001", "x32")]))))
557
558+ def test_run_with_architectures(self):
559+ # If the user explicitly requested architectures, the job passes
560+ # those through when requesting builds, intersecting them with other
561+ # constraints.
562+ distroseries, processors = self.makeSeriesAndProcessors(
563+ ["avr2001", "sparc64", "x32"])
564+ [git_ref] = self.factory.makeGitRefs()
565+ snap = self.factory.makeSnap(
566+ git_ref=git_ref, distroseries=distroseries, processors=processors)
567+ expected_date_created = get_transaction_timestamp(IStore(snap))
568+ job = SnapRequestBuildsJob.create(
569+ snap, snap.registrant, distroseries.main_archive,
570+ PackagePublishingPocket.RELEASE, {"core": "stable"},
571+ architectures=["sparc64", "x32"])
572+ snapcraft_yaml = dedent("""\
573+ architectures:
574+ - build-on: avr2001
575+ - build-on: x32
576+ """)
577+ self.useFixture(GitHostingFixture(blob=snapcraft_yaml))
578+ with dbuser(config.ISnapRequestBuildsJobSource.dbuser):
579+ JobRunner([job]).runAll()
580+ now = get_transaction_timestamp(IStore(snap))
581+ self.assertEmailQueueLength(0)
582+ self.assertThat(job, MatchesStructure(
583+ job=MatchesStructure.byEquality(status=JobStatus.COMPLETED),
584+ date_created=Equals(expected_date_created),
585+ date_finished=MatchesAll(
586+ GreaterThan(expected_date_created), LessThan(now)),
587+ error_message=Is(None),
588+ builds=AfterPreprocessing(set, MatchesSetwise(
589+ MatchesStructure(
590+ build_request=MatchesStructure.byEquality(id=job.job.id),
591+ requester=Equals(snap.registrant),
592+ snap=Equals(snap),
593+ archive=Equals(distroseries.main_archive),
594+ distro_arch_series=Equals(distroseries["x32"]),
595+ pocket=Equals(PackagePublishingPocket.RELEASE),
596+ channels=Equals({"core": "stable"}))))))
597+
598 def test_run_failed(self):
599 # A failed run sets the job status to FAILED and records the error
600 # message.