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
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2019-05-16 10:21:14 +0000
+++ lib/lp/snappy/browser/snap.py 2019-06-21 11:37:21 +0000
@@ -57,7 +57,6 @@
57from lp.registry.enums import VCSType57from lp.registry.enums import VCSType
58from lp.registry.interfaces.pocket import PackagePublishingPocket58from lp.registry.interfaces.pocket import PackagePublishingPocket
59from lp.services.features import getFeatureFlag59from lp.services.features import getFeatureFlag
60from lp.services.helpers import english_list
61from lp.services.propertycache import cachedproperty60from lp.services.propertycache import cachedproperty
62from lp.services.scripts import log61from lp.services.scripts import log
63from lp.services.utils import seconds_since_epoch62from lp.services.utils import seconds_since_epoch
@@ -92,7 +91,6 @@
92 MissingSnapcraftYaml,91 MissingSnapcraftYaml,
93 NoSuchSnap,92 NoSuchSnap,
94 SNAP_PRIVATE_FEATURE_FLAG,93 SNAP_PRIVATE_FEATURE_FLAG,
95 SnapBuildAlreadyPending,
96 SnapPrivateFeatureDisabled,94 SnapPrivateFeatureDisabled,
97 )95 )
98from lp.snappy.interfaces.snapbuild import ISnapBuildSet96from lp.snappy.interfaces.snapbuild import ISnapBuildSet
@@ -256,20 +254,6 @@
256 return items254 return items
257255
258256
259def new_builds_notification_text(builds, already_pending=None):
260 nr_builds = len(builds)
261 if not nr_builds:
262 builds_text = "All requested builds are already queued."
263 elif nr_builds == 1:
264 builds_text = "1 new build has been queued."
265 else:
266 builds_text = "%d new builds have been queued." % nr_builds
267 if nr_builds and already_pending:
268 return structured("<p>%s</p><p>%s</p>", builds_text, already_pending)
269 else:
270 return builds_text
271
272
273class SnapRequestBuildsView(LaunchpadFormView):257class SnapRequestBuildsView(LaunchpadFormView):
274 """A view for requesting builds of a snap package."""258 """A view for requesting builds of a snap package."""
275259
@@ -328,45 +312,18 @@
328 'channels': self.context.auto_build_channels,312 'channels': self.context.auto_build_channels,
329 }313 }
330314
331 def requestBuild(self, data):
332 """User action for requesting a number of builds.
333
334 We raise exceptions for most errors, but if there's already a
335 pending build for a particular architecture, we simply record that
336 so that other builds can be queued and a message displayed to the
337 caller.
338 """
339 informational = {}
340 builds = []
341 already_pending = []
342 for arch in data['distro_arch_series']:
343 try:
344 build = self.context.requestBuild(
345 self.user, data['archive'], arch, data['pocket'],
346 channels=data['channels'])
347 builds.append(build)
348 except SnapBuildAlreadyPending:
349 already_pending.append(arch)
350 if already_pending:
351 informational['already_pending'] = (
352 "An identical build is already pending for %s." %
353 english_list(arch.architecturetag for arch in already_pending))
354 return builds, informational
355
356 @action('Request builds', name='request')315 @action('Request builds', name='request')
357 def request_action(self, action, data):316 def request_action(self, action, data):
358 if data.get('distro_arch_series', []):317 if data.get('distro_arch_series', []):
359 builds, informational = self.requestBuild(data)318 architectures = [
360 already_pending = informational.get('already_pending')319 arch.architecturetag for arch in data['distro_arch_series']]
361 notification_text = new_builds_notification_text(
362 builds, already_pending)
363 self.request.response.addNotification(notification_text)
364 else:320 else:
365 self.context.requestBuilds(321 architectures = None
366 self.user, data['archive'], data['pocket'],322 self.context.requestBuilds(
367 channels=data['channels'])323 self.user, data['archive'], data['pocket'],
368 self.request.response.addNotification(324 architectures=architectures, channels=data['channels'])
369 _('Builds will be dispatched soon.'))325 self.request.response.addNotification(
326 _('Builds will be dispatched soon.'))
370 self.next_url = self.cancel_url327 self.next_url = self.cancel_url
371328
372329
373330
=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 2019-06-11 09:39:29 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2019-06-21 11:37:21 +0000
@@ -29,6 +29,7 @@
29 AfterPreprocessing,29 AfterPreprocessing,
30 Equals,30 Equals,
31 Is,31 Is,
32 MatchesDict,
32 MatchesListwise,33 MatchesListwise,
33 MatchesSetwise,34 MatchesSetwise,
34 MatchesStructure,35 MatchesStructure,
@@ -1690,8 +1691,8 @@
1690 Unauthorized, self.getViewBrowser, self.snap, "+request-builds")1691 Unauthorized, self.getViewBrowser, self.snap, "+request-builds")
16911692
1692 def test_request_builds_with_architectures_action(self):1693 def test_request_builds_with_architectures_action(self):
1693 # Requesting a build with architectures selected creates pending1694 # Requesting a build with architectures selected creates a pending
1694 # builds.1695 # build request limited to those architectures.
1695 browser = self.getViewBrowser(1696 browser = self.getViewBrowser(
1696 self.snap, "+request-builds", user=self.person)1697 self.snap, "+request-builds", user=self.person)
1697 browser.getControl("amd64").selected = True1698 browser.getControl("amd64").selected = True
@@ -1699,21 +1700,24 @@
1699 browser.getControl("Request builds").click()1700 browser.getControl("Request builds").click()
17001701
1701 login_person(self.person)1702 login_person(self.person)
1702 builds = self.snap.pending_builds1703 [request] = self.snap.pending_build_requests
1703 self.assertContentEqual(1704 self.assertThat(removeSecurityProxy(request), MatchesStructure(
1704 [self.ubuntu.main_archive], set(build.archive for build in builds))1705 snap=Equals(self.snap),
1705 self.assertContentEqual(1706 status=Equals(SnapBuildRequestStatus.PENDING),
1706 ["amd64", "i386"],1707 error_message=Is(None),
1707 [build.distro_arch_series.architecturetag for build in builds])1708 builds=AfterPreprocessing(list, Equals([])),
1708 self.assertContentEqual(1709 archive=Equals(self.ubuntu.main_archive),
1709 [PackagePublishingPocket.UPDATES],1710 _job=MatchesStructure(
1710 set(build.pocket for build in builds))1711 requester=Equals(self.person),
1711 self.assertContentEqual(1712 pocket=Equals(PackagePublishingPocket.UPDATES),
1712 [2510], set(build.buildqueue_record.lastscore for build in builds))1713 channels=Equals({}),
1714 architectures=MatchesSetwise(Equals("amd64"), Equals("i386")),
1715 )))
17131716
1714 def test_request_builds_with_architectures_ppa(self):1717 def test_request_builds_with_architectures_ppa(self):
1715 # Selecting a different archive with architectures selected creates1718 # Selecting a different archive with architectures selected creates
1716 # builds in that archive.1719 # a build request targeting that archive and limited to those
1720 # architectures.
1717 ppa = self.factory.makeArchive(1721 ppa = self.factory.makeArchive(
1718 distribution=self.ubuntu, owner=self.person, name="snap-ppa")1722 distribution=self.ubuntu, owner=self.person, name="snap-ppa")
1719 browser = self.getViewBrowser(1723 browser = self.getViewBrowser(
@@ -1725,12 +1729,15 @@
1725 browser.getControl("Request builds").click()1729 browser.getControl("Request builds").click()
17261730
1727 login_person(self.person)1731 login_person(self.person)
1728 builds = self.snap.pending_builds1732 [request] = self.snap.pending_build_requests
1729 self.assertEqual([ppa], [build.archive for build in builds])1733 self.assertThat(request, MatchesStructure(
1734 archive=Equals(ppa),
1735 architectures=MatchesSetwise(Equals("amd64"))))
17301736
1731 def test_request_builds_with_architectures_channels(self):1737 def test_request_builds_with_architectures_channels(self):
1732 # Selecting different channels with architectures selected creates1738 # Selecting different channels with architectures selected creates a
1733 # builds using those channels.1739 # build request using those channels and limited to those
1740 # architectures.
1734 browser = self.getViewBrowser(1741 browser = self.getViewBrowser(
1735 self.snap, "+request-builds", user=self.person)1742 self.snap, "+request-builds", user=self.person)
1736 browser.getControl(name="field.channels.core").value = "edge"1743 browser.getControl(name="field.channels.core").value = "edge"
@@ -1739,25 +1746,10 @@
1739 browser.getControl("Request builds").click()1746 browser.getControl("Request builds").click()
17401747
1741 login_person(self.person)1748 login_person(self.person)
1742 builds = self.snap.pending_builds1749 [request] = self.snap.pending_build_requests
1743 self.assertEqual(1750 self.assertThat(request, MatchesStructure(
1744 [{"core": "edge"}], [build.channels for build in builds])1751 channels=MatchesDict({"core": Equals("edge")}),
17451752 architectures=MatchesSetwise(Equals("amd64"))))
1746 def test_request_builds_with_architectures_rejects_duplicate(self):
1747 # A duplicate build request with architectures selected causes a
1748 # notification.
1749 self.snap.requestBuild(
1750 self.person, self.ubuntu.main_archive, self.distroseries["amd64"],
1751 PackagePublishingPocket.UPDATES)
1752 browser = self.getViewBrowser(
1753 self.snap, "+request-builds", user=self.person)
1754 browser.getControl("amd64").selected = True
1755 browser.getControl("i386").selected = True
1756 browser.getControl("Request builds").click()
1757 main_text = extract_text(find_main_content(browser.contents))
1758 self.assertIn("1 new build has been queued.", main_text)
1759 self.assertIn(
1760 "An identical build is already pending for amd64.", main_text)
17611753
1762 def test_request_builds_no_architectures_action(self):1754 def test_request_builds_no_architectures_action(self):
1763 # Requesting a build with no architectures selected creates a1755 # Requesting a build with no architectures selected creates a
@@ -1779,7 +1771,8 @@
1779 _job=MatchesStructure(1771 _job=MatchesStructure(
1780 requester=Equals(self.person),1772 requester=Equals(self.person),
1781 pocket=Equals(PackagePublishingPocket.UPDATES),1773 pocket=Equals(PackagePublishingPocket.UPDATES),
1782 channels=Equals({}))))1774 channels=Equals({}),
1775 architectures=Is(None))))
17831776
1784 def test_request_builds_no_architectures_ppa(self):1777 def test_request_builds_no_architectures_ppa(self):
1785 # Selecting a different archive with no architectures selected1778 # Selecting a different archive with no architectures selected
@@ -1796,7 +1789,9 @@
17961789
1797 login_person(self.person)1790 login_person(self.person)
1798 [request] = self.snap.pending_build_requests1791 [request] = self.snap.pending_build_requests
1799 self.assertEqual(ppa, request.archive)1792 self.assertThat(request, MatchesStructure(
1793 archive=Equals(ppa),
1794 architectures=Is(None)))
18001795
1801 def test_request_builds_no_architectures_channels(self):1796 def test_request_builds_no_architectures_channels(self):
1802 # Selecting different channels with no architectures selected1797 # Selecting different channels with no architectures selected
18031798
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2019-05-22 18:33:10 +0000
+++ lib/lp/snappy/interfaces/snap.py 2019-06-21 11:37:21 +0000
@@ -76,6 +76,7 @@
76 Dict,76 Dict,
77 Int,77 Int,
78 List,78 List,
79 Set,
79 Text,80 Text,
80 TextLine,81 TextLine,
81 )82 )
@@ -336,6 +337,10 @@
336 title=_("Source snap channels for builds produced by this request"),337 title=_("Source snap channels for builds produced by this request"),
337 key_type=TextLine(), required=False, readonly=True)338 key_type=TextLine(), required=False, readonly=True)
338339
340 architectures = Set(
341 title=_("If set, this request is limited to these architecture tags"),
342 value_type=TextLine(), required=False, readonly=True)
343
339344
340class ISnapView(Interface):345class ISnapView(Interface):
341 """`ISnap` attributes that require launchpad.View permission."""346 """`ISnap` attributes that require launchpad.View permission."""
@@ -437,8 +442,9 @@
437 """442 """
438443
439 def requestBuildsFromJob(requester, archive, pocket, channels=None,444 def requestBuildsFromJob(requester, archive, pocket, channels=None,
440 allow_failures=False, fetch_snapcraft_yaml=True,445 architectures=None, allow_failures=False,
441 build_request=None, logger=None):446 fetch_snapcraft_yaml=True, build_request=None,
447 logger=None):
442 """Synchronous part of `Snap.requestBuilds`.448 """Synchronous part of `Snap.requestBuilds`.
443449
444 Request that the snap package be built for relevant architectures.450 Request that the snap package be built for relevant architectures.
@@ -448,6 +454,9 @@
448 :param pocket: The pocket that should be targeted.454 :param pocket: The pocket that should be targeted.
449 :param channels: A dictionary mapping snap names to channels to use455 :param channels: A dictionary mapping snap names to channels to use
450 for these builds.456 for these builds.
457 :param architectures: If not None, limit builds to architectures
458 with these architecture tags (in addition to any other
459 applicable constraints).
451 :param allow_failures: If True, log exceptions other than "already460 :param allow_failures: If True, log exceptions other than "already
452 pending" from individual build requests; if False, raise them to461 pending" from individual build requests; if False, raise them to
453 the caller.462 the caller.
454463
=== modified file 'lib/lp/snappy/interfaces/snapjob.py'
--- lib/lp/snappy/interfaces/snapjob.py 2019-05-22 18:33:10 +0000
+++ lib/lp/snappy/interfaces/snapjob.py 2019-06-21 11:37:21 +0000
@@ -22,6 +22,7 @@
22 Datetime,22 Datetime,
23 Dict,23 Dict,
24 List,24 List,
25 Set,
25 TextLine,26 TextLine,
26 )27 )
2728
@@ -78,6 +79,10 @@
78 "are supported."),79 "are supported."),
79 key_type=TextLine(), required=False, readonly=True)80 key_type=TextLine(), required=False, readonly=True)
8081
82 architectures = Set(
83 title=_("If set, limit builds to these architecture tags."),
84 value_type=TextLine(), required=False, readonly=True)
85
81 date_created = Datetime(86 date_created = Datetime(
82 title=_("Time when this job was created."),87 title=_("Time when this job was created."),
83 required=True, readonly=True)88 required=True, readonly=True)
@@ -101,7 +106,7 @@
101106
102class ISnapRequestBuildsJobSource(IJobSource):107class ISnapRequestBuildsJobSource(IJobSource):
103108
104 def create(snap, requester, archive, pocket, channels):109 def create(snap, requester, archive, pocket, channels, architectures=None):
105 """Request builds of a snap package.110 """Request builds of a snap package.
106111
107 :param snap: The snap package to build.112 :param snap: The snap package to build.
@@ -110,6 +115,9 @@
110 :param pocket: The pocket that should be targeted.115 :param pocket: The pocket that should be targeted.
111 :param channels: A dictionary mapping snap names to channels to use116 :param channels: A dictionary mapping snap names to channels to use
112 for these builds.117 for these builds.
118 :param architectures: If not None, limit builds to architectures
119 with these architecture tags (in addition to any other
120 applicable constraints).
113 """121 """
114122
115 def findBySnap(snap, statuses=None, job_ids=None):123 def findBySnap(snap, statuses=None, job_ids=None):
116124
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2019-05-16 10:21:14 +0000
+++ lib/lp/snappy/model/snap.py 2019-06-21 11:37:21 +0000
@@ -249,6 +249,11 @@
249 """See `ISnapBuildRequest`."""249 """See `ISnapBuildRequest`."""
250 return self._job.channels250 return self._job.channels
251251
252 @property
253 def architectures(self):
254 """See `ISnapBuildRequest`."""
255 return self._job.architectures
256
252257
253@implementer(ISnap, IHasOwner)258@implementer(ISnap, IHasOwner)
254class Snap(Storm, WebhookTargetMixin):259class Snap(Storm, WebhookTargetMixin):
@@ -655,11 +660,13 @@
655 notify(ObjectCreatedEvent(build, user=requester))660 notify(ObjectCreatedEvent(build, user=requester))
656 return build661 return build
657662
658 def requestBuilds(self, requester, archive, pocket, channels=None):663 def requestBuilds(self, requester, archive, pocket, channels=None,
664 architectures=None):
659 """See `ISnap`."""665 """See `ISnap`."""
660 self._checkRequestBuild(requester, archive)666 self._checkRequestBuild(requester, archive)
661 job = getUtility(ISnapRequestBuildsJobSource).create(667 job = getUtility(ISnapRequestBuildsJobSource).create(
662 self, requester, archive, pocket, channels)668 self, requester, archive, pocket, channels,
669 architectures=architectures)
663 return self.getBuildRequest(job.job_id)670 return self.getBuildRequest(job.job_id)
664671
665 @staticmethod672 @staticmethod
@@ -694,7 +701,8 @@
694 else:701 else:
695 return channels702 return channels
696703
697 def requestBuildsFromJob(self, requester, archive, pocket, channels=None,704 def requestBuildsFromJob(self, requester, archive, pocket,
705 channels=None, architectures=None,
698 allow_failures=False, fetch_snapcraft_yaml=True,706 allow_failures=False, fetch_snapcraft_yaml=True,
699 build_request=None, logger=None):707 build_request=None, logger=None):
700 """See `ISnap`."""708 """See `ISnap`."""
@@ -731,7 +739,9 @@
731 supported_arches = OrderedDict(739 supported_arches = OrderedDict(
732 (das.architecturetag, das) for das in sorted(740 (das.architecturetag, das) for das in sorted(
733 self.getAllowedArchitectures(distro_series),741 self.getAllowedArchitectures(distro_series),
734 key=attrgetter("processor.id")))742 key=attrgetter("processor.id"))
743 if (architectures is None or
744 das.architecturetag in architectures))
735 architectures_to_build = determine_architectures_to_build(745 architectures_to_build = determine_architectures_to_build(
736 snapcraft_data, supported_arches.keys())746 snapcraft_data, supported_arches.keys())
737 except Exception as e:747 except Exception as e:
738748
=== modified file 'lib/lp/snappy/model/snapjob.py'
--- lib/lp/snappy/model/snapjob.py 2018-10-09 09:25:19 +0000
+++ lib/lp/snappy/model/snapjob.py 2019-06-21 11:37:21 +0000
@@ -1,4 +1,4 @@
1# Copyright 2018 Canonical Ltd. This software is licensed under the1# Copyright 2018-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Snap package jobs."""4"""Snap package jobs."""
@@ -184,13 +184,18 @@
184 config = config.ISnapRequestBuildsJobSource184 config = config.ISnapRequestBuildsJobSource
185185
186 @classmethod186 @classmethod
187 def create(cls, snap, requester, archive, pocket, channels):187 def create(cls, snap, requester, archive, pocket, channels,
188 architectures=None):
188 """See `ISnapRequestBuildsJobSource`."""189 """See `ISnapRequestBuildsJobSource`."""
189 metadata = {190 metadata = {
190 "requester": requester.id,191 "requester": requester.id,
191 "archive": archive.id,192 "archive": archive.id,
192 "pocket": pocket.value,193 "pocket": pocket.value,
193 "channels": channels,194 "channels": channels,
195 # Really a set or None, but sets aren't directly
196 # JSON-serialisable.
197 "architectures": (
198 list(architectures) if architectures is not None else None),
194 }199 }
195 snap_job = SnapJob(snap, cls.class_job_type, metadata)200 snap_job = SnapJob(snap, cls.class_job_type, metadata)
196 job = cls(snap_job)201 job = cls(snap_job)
@@ -292,6 +297,12 @@
292 return self.metadata["channels"]297 return self.metadata["channels"]
293298
294 @property299 @property
300 def architectures(self):
301 """See `ISnapRequestBuildsJob`."""
302 architectures = self.metadata["architectures"]
303 return set(architectures) if architectures is not None else None
304
305 @property
295 def date_created(self):306 def date_created(self):
296 """See `ISnapRequestBuildsJob`."""307 """See `ISnapRequestBuildsJob`."""
297 return self.context.job.date_created308 return self.context.job.date_created
@@ -346,6 +357,7 @@
346 try:357 try:
347 self.builds = self.snap.requestBuildsFromJob(358 self.builds = self.snap.requestBuildsFromJob(
348 requester, archive, self.pocket, channels=self.channels,359 requester, archive, self.pocket, channels=self.channels,
360 architectures=self.architectures,
349 build_request=self.build_request, logger=log)361 build_request=self.build_request, logger=log)
350 self.error_message = None362 self.error_message = None
351 except self.retry_error_types:363 except self.retry_error_types:
352364
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2019-06-20 13:16:35 +0000
+++ lib/lp/snappy/tests/test_snap.py 2019-06-21 11:37:21 +0000
@@ -475,7 +475,9 @@
475 status=Equals(SnapBuildRequestStatus.PENDING),475 status=Equals(SnapBuildRequestStatus.PENDING),
476 error_message=Is(None),476 error_message=Is(None),
477 builds=AfterPreprocessing(set, MatchesSetwise()),477 builds=AfterPreprocessing(set, MatchesSetwise()),
478 archive=Equals(snap.distro_series.main_archive)))478 archive=Equals(snap.distro_series.main_archive),
479 channels=MatchesDict({"snapcraft": Equals("edge")}),
480 architectures=Is(None)))
479 [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()481 [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()
480 self.assertThat(job, MatchesStructure(482 self.assertThat(job, MatchesStructure(
481 job_id=Equals(request.id),483 job_id=Equals(request.id),
@@ -484,7 +486,8 @@
484 requester=Equals(snap.owner.teamowner),486 requester=Equals(snap.owner.teamowner),
485 archive=Equals(snap.distro_series.main_archive),487 archive=Equals(snap.distro_series.main_archive),
486 pocket=Equals(PackagePublishingPocket.UPDATES),488 pocket=Equals(PackagePublishingPocket.UPDATES),
487 channels=Equals({"snapcraft": "edge"})))489 channels=Equals({"snapcraft": "edge"}),
490 architectures=Is(None)))
488491
489 def test_requestBuilds_without_distroseries(self):492 def test_requestBuilds_without_distroseries(self):
490 # requestBuilds schedules a job for a snap without a distroseries.493 # requestBuilds schedules a job for a snap without a distroseries.
@@ -502,16 +505,51 @@
502 status=Equals(SnapBuildRequestStatus.PENDING),505 status=Equals(SnapBuildRequestStatus.PENDING),
503 error_message=Is(None),506 error_message=Is(None),
504 builds=AfterPreprocessing(set, MatchesSetwise()),507 builds=AfterPreprocessing(set, MatchesSetwise()),
505 archive=Equals(archive)))508 archive=Equals(archive),
506 [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()509 channels=MatchesDict({"snapcraft": Equals("edge")}),
507 self.assertThat(job, MatchesStructure(510 architectures=Is(None)))
508 job_id=Equals(request.id),511 [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()
509 job=MatchesStructure.byEquality(status=JobStatus.WAITING),512 self.assertThat(job, MatchesStructure(
510 snap=Equals(snap),513 job_id=Equals(request.id),
511 requester=Equals(snap.owner.teamowner),514 job=MatchesStructure.byEquality(status=JobStatus.WAITING),
512 archive=Equals(archive),515 snap=Equals(snap),
513 pocket=Equals(PackagePublishingPocket.UPDATES),516 requester=Equals(snap.owner.teamowner),
514 channels=Equals({"snapcraft": "edge"})))517 archive=Equals(archive),
518 pocket=Equals(PackagePublishingPocket.UPDATES),
519 channels=Equals({"snapcraft": "edge"}),
520 architectures=Is(None)))
521
522 def test_requestBuilds_with_architectures(self):
523 # If asked to build for particular architectures, requestBuilds
524 # passes those through to the job.
525 snap = self.factory.makeSnap()
526 now = get_transaction_timestamp(IStore(snap))
527 with person_logged_in(snap.owner.teamowner):
528 request = snap.requestBuilds(
529 snap.owner.teamowner, snap.distro_series.main_archive,
530 PackagePublishingPocket.UPDATES,
531 channels={"snapcraft": "edge"},
532 architectures={"amd64", "i386"})
533 self.assertThat(request, MatchesStructure(
534 date_requested=Equals(now),
535 date_finished=Is(None),
536 snap=Equals(snap),
537 status=Equals(SnapBuildRequestStatus.PENDING),
538 error_message=Is(None),
539 builds=AfterPreprocessing(set, MatchesSetwise()),
540 archive=Equals(snap.distro_series.main_archive),
541 channels=MatchesDict({"snapcraft": Equals("edge")}),
542 architectures=MatchesSetwise(Equals("amd64"), Equals("i386"))))
543 [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()
544 self.assertThat(job, MatchesStructure(
545 job_id=Equals(request.id),
546 job=MatchesStructure.byEquality(status=JobStatus.WAITING),
547 snap=Equals(snap),
548 requester=Equals(snap.owner.teamowner),
549 archive=Equals(snap.distro_series.main_archive),
550 pocket=Equals(PackagePublishingPocket.UPDATES),
551 channels=Equals({"snapcraft": "edge"}),
552 architectures=MatchesSetwise(Equals("amd64"), Equals("i386"))))
515553
516 def test__findBase(self):554 def test__findBase(self):
517 snap_base_set = getUtility(ISnapBaseSet)555 snap_base_set = getUtility(ISnapBaseSet)
@@ -585,7 +623,7 @@
585 with person_logged_in(job.requester):623 with person_logged_in(job.requester):
586 builds = job.snap.requestBuildsFromJob(624 builds = job.snap.requestBuildsFromJob(
587 job.requester, job.archive, job.pocket,625 job.requester, job.archive, job.pocket,
588 removeSecurityProxy(job.channels),626 channels=removeSecurityProxy(job.channels),
589 build_request=job.build_request)627 build_request=job.build_request)
590 self.assertRequestedBuildsMatch(builds, job, ["sparc"], job.channels)628 self.assertRequestedBuildsMatch(builds, job, ["sparc"], job.channels)
591629
@@ -601,11 +639,29 @@
601 with person_logged_in(job.requester):639 with person_logged_in(job.requester):
602 builds = job.snap.requestBuildsFromJob(640 builds = job.snap.requestBuildsFromJob(
603 job.requester, job.archive, job.pocket,641 job.requester, job.archive, job.pocket,
604 removeSecurityProxy(job.channels),642 channels=removeSecurityProxy(job.channels),
605 build_request=job.build_request)643 build_request=job.build_request)
606 self.assertRequestedBuildsMatch(644 self.assertRequestedBuildsMatch(
607 builds, job, ["mips64el", "riscv64"], job.channels)645 builds, job, ["mips64el", "riscv64"], job.channels)
608646
647 def test_requestBuildsFromJob_architectures_parameter(self):
648 # If an explicit set of architectures was given as a parameter,
649 # requestBuildsFromJob intersects those with any other constraints
650 # when requesting builds.
651 self.useFixture(GitHostingFixture(blob="name: foo\n"))
652 job = self.makeRequestBuildsJob(["avr", "mips64el", "riscv64"])
653 self.assertEqual(
654 get_transaction_timestamp(IStore(job.snap)), job.date_created)
655 transaction.commit()
656 with person_logged_in(job.requester):
657 builds = job.snap.requestBuildsFromJob(
658 job.requester, job.archive, job.pocket,
659 channels=removeSecurityProxy(job.channels),
660 architectures={"avr", "riscv64"},
661 build_request=job.build_request)
662 self.assertRequestedBuildsMatch(
663 builds, job, ["avr", "riscv64"], job.channels)
664
609 def test_requestBuildsFromJob_no_distroseries_explicit_base(self):665 def test_requestBuildsFromJob_no_distroseries_explicit_base(self):
610 # If the snap doesn't specify a distroseries but has an explicit666 # If the snap doesn't specify a distroseries but has an explicit
611 # base, requestBuildsFromJob requests builds for the appropriate667 # base, requestBuildsFromJob requests builds for the appropriate
612668
=== modified file 'lib/lp/snappy/tests/test_snapjob.py'
--- lib/lp/snappy/tests/test_snapjob.py 2018-09-10 11:18:42 +0000
+++ lib/lp/snappy/tests/test_snapjob.py 2019-06-21 11:37:21 +0000
@@ -1,4 +1,4 @@
1# Copyright 2018 Canonical Ltd. This software is licensed under the1# Copyright 2018-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for snap package jobs."""4"""Tests for snap package jobs."""
@@ -133,6 +133,46 @@
133 channels=Equals({"core": "stable"}))133 channels=Equals({"core": "stable"}))
134 for arch in ("avr2001", "x32")]))))134 for arch in ("avr2001", "x32")]))))
135135
136 def test_run_with_architectures(self):
137 # If the user explicitly requested architectures, the job passes
138 # those through when requesting builds, intersecting them with other
139 # constraints.
140 distroseries, processors = self.makeSeriesAndProcessors(
141 ["avr2001", "sparc64", "x32"])
142 [git_ref] = self.factory.makeGitRefs()
143 snap = self.factory.makeSnap(
144 git_ref=git_ref, distroseries=distroseries, processors=processors)
145 expected_date_created = get_transaction_timestamp(IStore(snap))
146 job = SnapRequestBuildsJob.create(
147 snap, snap.registrant, distroseries.main_archive,
148 PackagePublishingPocket.RELEASE, {"core": "stable"},
149 architectures=["sparc64", "x32"])
150 snapcraft_yaml = dedent("""\
151 architectures:
152 - build-on: avr2001
153 - build-on: x32
154 """)
155 self.useFixture(GitHostingFixture(blob=snapcraft_yaml))
156 with dbuser(config.ISnapRequestBuildsJobSource.dbuser):
157 JobRunner([job]).runAll()
158 now = get_transaction_timestamp(IStore(snap))
159 self.assertEmailQueueLength(0)
160 self.assertThat(job, MatchesStructure(
161 job=MatchesStructure.byEquality(status=JobStatus.COMPLETED),
162 date_created=Equals(expected_date_created),
163 date_finished=MatchesAll(
164 GreaterThan(expected_date_created), LessThan(now)),
165 error_message=Is(None),
166 builds=AfterPreprocessing(set, MatchesSetwise(
167 MatchesStructure(
168 build_request=MatchesStructure.byEquality(id=job.job.id),
169 requester=Equals(snap.registrant),
170 snap=Equals(snap),
171 archive=Equals(distroseries.main_archive),
172 distro_arch_series=Equals(distroseries["x32"]),
173 pocket=Equals(PackagePublishingPocket.RELEASE),
174 channels=Equals({"core": "stable"}))))))
175
136 def test_run_failed(self):176 def test_run_failed(self):
137 # A failed run sets the job status to FAILED and records the error177 # A failed run sets the job status to FAILED and records the error
138 # message.178 # message.