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