Merge lp:~cjwatson/launchpad/snap-without-distro-series into lp:launchpad
- snap-without-distro-series
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18884 | ||||
Proposed branch: | lp:~cjwatson/launchpad/snap-without-distro-series | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/base-snap | ||||
Diff against target: |
854 lines (+395/-60) 10 files modified
lib/lp/snappy/browser/snap.py (+17/-4) lib/lp/snappy/browser/tests/test_snap.py (+38/-3) lib/lp/snappy/browser/widgets/snaparchive.py (+3/-2) lib/lp/snappy/browser/widgets/tests/test_snaparchivewidget.py (+13/-7) lib/lp/snappy/interfaces/snap.py (+16/-6) lib/lp/snappy/model/snap.py (+99/-20) lib/lp/snappy/templates/snap-index.pt (+4/-2) lib/lp/snappy/templates/snap-request-builds.pt (+11/-3) lib/lp/snappy/tests/test_snap.py (+191/-10) lib/lp/testing/factory.py (+3/-3) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-without-distro-series | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+362730@code.launchpad.net |
Commit message
Support building snaps without a configured distro series.
Description of the change
In this case, the series is inferred from the base snap given by snapcraft.yaml, if any.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
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-01-30 12:28:49 +0000 |
3 | +++ lib/lp/snappy/browser/snap.py 2019-02-18 14:51:25 +0000 |
4 | @@ -1,4 +1,4 @@ |
5 | -# Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
6 | +# Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
7 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | |
9 | """Snap views.""" |
10 | @@ -44,6 +44,7 @@ |
11 | from lp.app.browser.tales import format_link |
12 | from lp.app.enums import PRIVATE_INFORMATION_TYPES |
13 | from lp.app.interfaces.informationtype import IInformationType |
14 | +from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
15 | from lp.app.widgets.itemswidgets import ( |
16 | LabeledMultiCheckBoxWidget, |
17 | LaunchpadDropdownWidget, |
18 | @@ -310,7 +311,13 @@ |
19 | def initial_values(self): |
20 | """See `LaunchpadFormView`.""" |
21 | return { |
22 | - 'archive': self.context.distro_series.main_archive, |
23 | + 'archive': ( |
24 | + # XXX cjwatson 2019-02-04: In order to support non-Ubuntu |
25 | + # bases, we'd need to store this as None and infer it based |
26 | + # on the inferred distro series; but this will do for now. |
27 | + getUtility(ILaunchpadCelebrities).ubuntu.main_archive |
28 | + if self.context.distro_series is None |
29 | + else self.context.distro_series.main_archive), |
30 | 'distro_arch_series': [], |
31 | 'pocket': PackagePublishingPocket.UPDATES, |
32 | } |
33 | @@ -341,7 +348,7 @@ |
34 | |
35 | @action('Request builds', name='request') |
36 | def request_action(self, action, data): |
37 | - if data['distro_arch_series']: |
38 | + if data.get('distro_arch_series', []): |
39 | builds, informational = self.requestBuild(data) |
40 | already_pending = informational.get('already_pending') |
41 | notification_text = new_builds_notification_text( |
42 | @@ -498,7 +505,13 @@ |
43 | 'processors': [ |
44 | p for p in getUtility(IProcessorSet).getAll() |
45 | if p.build_by_default], |
46 | - 'auto_build_archive': distro_series.main_archive, |
47 | + 'auto_build_archive': ( |
48 | + # XXX cjwatson 2019-02-04: In order to support non-Ubuntu |
49 | + # bases, we'd need to store this as None and infer it based |
50 | + # on the inferred distro series; but this will do for now. |
51 | + getUtility(ILaunchpadCelebrities).ubuntu.main_archive |
52 | + if distro_series is None |
53 | + else distro_series.main_archive), |
54 | 'auto_build_pocket': PackagePublishingPocket.UPDATES, |
55 | } |
56 | |
57 | |
58 | === modified file 'lib/lp/snappy/browser/tests/test_snap.py' |
59 | --- lib/lp/snappy/browser/tests/test_snap.py 2019-01-30 12:28:49 +0000 |
60 | +++ lib/lp/snappy/browser/tests/test_snap.py 2019-02-18 14:51:25 +0000 |
61 | @@ -1,4 +1,4 @@ |
62 | -# Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
63 | +# Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
64 | # GNU Affero General Public License version 3 (see the file LICENSE). |
65 | |
66 | """Test snap package views.""" |
67 | @@ -1173,11 +1173,13 @@ |
68 | self.factory.makeBuilder(virtualized=True) |
69 | |
70 | def makeSnap(self, **kwargs): |
71 | + if "distroseries" not in kwargs: |
72 | + kwargs["distroseries"] = self.distroseries |
73 | if kwargs.get("branch") is None and kwargs.get("git_ref") is None: |
74 | kwargs["branch"] = self.factory.makeAnyBranch() |
75 | return self.factory.makeSnap( |
76 | - registrant=self.person, owner=self.person, |
77 | - distroseries=self.distroseries, name="snap-name", **kwargs) |
78 | + registrant=self.person, owner=self.person, name="snap-name", |
79 | + **kwargs) |
80 | |
81 | def makeBuild(self, snap=None, archive=None, date_created=None, **kwargs): |
82 | if snap is None: |
83 | @@ -1299,6 +1301,15 @@ |
84 | Primary Archive for Ubuntu Linux |
85 | """, self.getMainText(build.snap)) |
86 | |
87 | + def test_index_no_distro_series(self): |
88 | + # If the snap is configured to infer an appropriate distro series |
89 | + # from snapcraft.yaml, then the index page does not show a distro |
90 | + # series. |
91 | + snap = self.makeSnap(distroseries=None) |
92 | + text = self.getMainText(snap) |
93 | + self.assertIn("Snap package information", text) |
94 | + self.assertNotIn("Distribution series:", text) |
95 | + |
96 | def test_index_success_with_buildlog(self): |
97 | # The build log is shown if it is there. |
98 | build = self.makeBuild( |
99 | @@ -1383,6 +1394,8 @@ |
100 | store_upload_tag = soupmatchers.Tag( |
101 | "store upload", "div", attrs={"id": "store_upload"}) |
102 | self.assertThat(view(), soupmatchers.HTMLContains( |
103 | + soupmatchers.Tag( |
104 | + "distribution series", "dl", attrs={"id": "distro_series"}), |
105 | soupmatchers.Within( |
106 | store_upload_tag, |
107 | soupmatchers.Tag( |
108 | @@ -1629,3 +1642,25 @@ |
109 | login_person(self.person) |
110 | [request] = self.snap.pending_build_requests |
111 | self.assertEqual(ppa, request.archive) |
112 | + |
113 | + def test_request_builds_no_distro_series(self): |
114 | + # Requesting builds of a snap configured to infer an appropriate |
115 | + # distro series from snapcraft.yaml creates a pending build request. |
116 | + login_person(self.person) |
117 | + self.snap.distro_series = None |
118 | + browser = self.getViewBrowser( |
119 | + self.snap, "+request-builds", user=self.person) |
120 | + browser.getControl("Request builds").click() |
121 | + |
122 | + login_person(self.person) |
123 | + [request] = self.snap.pending_build_requests |
124 | + self.assertThat(removeSecurityProxy(request), MatchesStructure( |
125 | + snap=Equals(self.snap), |
126 | + status=Equals(SnapBuildRequestStatus.PENDING), |
127 | + error_message=Is(None), |
128 | + builds=AfterPreprocessing(list, Equals([])), |
129 | + archive=Equals(self.ubuntu.main_archive), |
130 | + _job=MatchesStructure( |
131 | + requester=Equals(self.person), |
132 | + pocket=Equals(PackagePublishingPocket.UPDATES), |
133 | + channels=Is(None)))) |
134 | |
135 | === modified file 'lib/lp/snappy/browser/widgets/snaparchive.py' |
136 | --- lib/lp/snappy/browser/widgets/snaparchive.py 2016-06-20 20:47:20 +0000 |
137 | +++ lib/lp/snappy/browser/widgets/snaparchive.py 2019-02-18 14:51:25 +0000 |
138 | @@ -1,4 +1,4 @@ |
139 | -# Copyright 2015-2016 Canonical Ltd. This software is licensed under the |
140 | +# Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
141 | # GNU Affero General Public License version 3 (see the file LICENSE). |
142 | |
143 | __metaclass__ = type |
144 | @@ -70,7 +70,8 @@ |
145 | |
146 | @property |
147 | def main_archive(self): |
148 | - if ISnap.providedBy(self.context.context): |
149 | + if (ISnap.providedBy(self.context.context) and |
150 | + self.context.context.distro_series is not None): |
151 | return self.context.context.distro_series.main_archive |
152 | else: |
153 | return getUtility(ILaunchpadCelebrities).ubuntu.main_archive |
154 | |
155 | === modified file 'lib/lp/snappy/browser/widgets/tests/test_snaparchivewidget.py' |
156 | --- lib/lp/snappy/browser/widgets/tests/test_snaparchivewidget.py 2017-11-10 11:28:43 +0000 |
157 | +++ lib/lp/snappy/browser/widgets/tests/test_snaparchivewidget.py 2019-02-18 14:51:25 +0000 |
158 | @@ -1,10 +1,11 @@ |
159 | -# Copyright 2015-2017 Canonical Ltd. This software is licensed under the |
160 | +# Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
161 | # GNU Affero General Public License version 3 (see the file LICENSE). |
162 | |
163 | from __future__ import absolute_import, print_function, unicode_literals |
164 | |
165 | __metaclass__ = type |
166 | |
167 | +from functools import partial |
168 | import re |
169 | |
170 | from lazr.restful.fields import Reference |
171 | @@ -36,8 +37,10 @@ |
172 | from lp.testing.layers import DatabaseFunctionalLayer |
173 | |
174 | |
175 | -def make_snap(test_case): |
176 | - return test_case.factory.makeSnap(distroseries=test_case.distroseries) |
177 | +def make_snap(test_case, **kwargs): |
178 | + if "distroseries" not in kwargs: |
179 | + kwargs["distroseries"] = test_case.distroseries |
180 | + return test_case.factory.makeSnap(**kwargs) |
181 | |
182 | |
183 | def make_branch(test_case): |
184 | @@ -54,6 +57,8 @@ |
185 | |
186 | scenarios = [ |
187 | ("Snap", {"context_factory": make_snap}), |
188 | + ("Snap with no distroseries", |
189 | + {"context_factory": partial(make_snap, distroseries=None)}), |
190 | ("Branch", {"context_factory": make_branch}), |
191 | ("GitRepository", {"context_factory": make_git_repository}), |
192 | ] |
193 | @@ -219,12 +224,13 @@ |
194 | |
195 | def test_getInputValue_primary(self): |
196 | # When the primary radio button is selected, the field value is the |
197 | - # context's primary archive if the context is a Snap, or the Ubuntu |
198 | - # primary archive otherwise. |
199 | + # context's primary archive if the context is a Snap and has a |
200 | + # distroseries, or the Ubuntu primary archive otherwise. |
201 | self.widget.request = LaunchpadTestRequest( |
202 | form={"field.archive": "primary"}) |
203 | - if ISnap.providedBy(self.context): |
204 | - expected_main_archive = self.distroseries.main_archive |
205 | + if (ISnap.providedBy(self.context) and |
206 | + self.context.distro_series is not None): |
207 | + expected_main_archive = self.context.distro_series.main_archive |
208 | else: |
209 | expected_main_archive = ( |
210 | getUtility(ILaunchpadCelebrities).ubuntu.main_archive) |
211 | |
212 | === modified file 'lib/lp/snappy/interfaces/snap.py' |
213 | --- lib/lp/snappy/interfaces/snap.py 2018-11-07 18:12:08 +0000 |
214 | +++ lib/lp/snappy/interfaces/snap.py 2019-02-18 14:51:25 +0000 |
215 | @@ -1,4 +1,4 @@ |
216 | -# Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
217 | +# Copyright 2015-2019 Canonical Ltd. This software is licensed under the |
218 | # GNU Affero General Public License version 3 (see the file LICENSE). |
219 | |
220 | """Snap package interfaces.""" |
221 | @@ -153,10 +153,10 @@ |
222 | class SnapBuildDisallowedArchitecture(Exception): |
223 | """A build was requested for a disallowed architecture.""" |
224 | |
225 | - def __init__(self, das): |
226 | + def __init__(self, das, pocket): |
227 | super(SnapBuildDisallowedArchitecture, self).__init__( |
228 | - "This snap package is not allowed to build for %s." % |
229 | - das.displayname) |
230 | + "This snap package is not allowed to build for %s/%s." % |
231 | + (das.distroseries.getSuite(pocket), das.architecturetag)) |
232 | |
233 | |
234 | @error_status(httplib.UNAUTHORIZED) |
235 | @@ -453,6 +453,8 @@ |
236 | :param build_request: The `ISnapBuildRequest` job being processed, |
237 | if any. |
238 | :param logger: An optional logger. |
239 | + :raises CannotRequestAutoBuilds: if fetch_snapcraft_yaml is False |
240 | + and self.distro_series is not set. |
241 | :return: A sequence of `ISnapBuild` instances. |
242 | """ |
243 | |
244 | @@ -547,6 +549,10 @@ |
245 | logger=None): |
246 | """Create and return automatic builds for this snap package. |
247 | |
248 | + This webservice API method is deprecated. It is normally better to |
249 | + use the `requestBuilds` method instead, which can make dispatching |
250 | + decisions based on the contents of snapcraft.yaml. |
251 | + |
252 | :param allow_failures: If True, log exceptions other than "already |
253 | pending" from individual build requests; if False, raise them to |
254 | the caller. |
255 | @@ -557,6 +563,7 @@ |
256 | :param logger: An optional logger. |
257 | :raises CannotRequestAutoBuilds: if no auto_build_archive or |
258 | auto_build_pocket is set. |
259 | + :raises IncompatibleArguments: if no distro_series is set. |
260 | :return: A sequence of `ISnapBuild` instances. |
261 | """ |
262 | |
263 | @@ -632,9 +639,12 @@ |
264 | description=_("The owner of this snap package."))) |
265 | |
266 | distro_series = exported(Reference( |
267 | - IDistroSeries, title=_("Distro Series"), required=True, readonly=False, |
268 | + IDistroSeries, title=_("Distro Series"), |
269 | + required=False, readonly=False, |
270 | description=_( |
271 | - "The series for which the snap package should be built."))) |
272 | + "The series for which the snap package should be built. If not " |
273 | + "set, Launchpad will infer an appropriate series from " |
274 | + "snapcraft.yaml."))) |
275 | |
276 | name = exported(TextLine( |
277 | title=_("Name"), required=True, readonly=False, |
278 | |
279 | === modified file 'lib/lp/snappy/model/snap.py' |
280 | --- lib/lp/snappy/model/snap.py 2019-02-05 10:44:18 +0000 |
281 | +++ lib/lp/snappy/model/snap.py 2019-02-18 14:51:25 +0000 |
282 | @@ -93,6 +93,8 @@ |
283 | IHasOwner, |
284 | IPersonRoles, |
285 | ) |
286 | +from lp.registry.model.distroseries import DistroSeries |
287 | +from lp.registry.model.series import ACTIVE_STATUSES |
288 | from lp.registry.model.teammembership import TeamParticipation |
289 | from lp.services.config import config |
290 | from lp.services.database.bulk import load_related |
291 | @@ -154,6 +156,10 @@ |
292 | SnapPrivacyMismatch, |
293 | SnapPrivateFeatureDisabled, |
294 | ) |
295 | +from lp.snappy.interfaces.snapbase import ( |
296 | + ISnapBaseSet, |
297 | + NoSuchSnapBase, |
298 | + ) |
299 | from lp.snappy.interfaces.snapbuild import ISnapBuildSet |
300 | from lp.snappy.interfaces.snapjob import ISnapRequestBuildsJobSource |
301 | from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet |
302 | @@ -258,7 +264,7 @@ |
303 | owner_id = Int(name='owner', allow_none=False) |
304 | owner = Reference(owner_id, 'Person.id') |
305 | |
306 | - distro_series_id = Int(name='distro_series', allow_none=False) |
307 | + distro_series_id = Int(name='distro_series', allow_none=True) |
308 | distro_series = Reference(distro_series_id, 'DistroSeries.id') |
309 | |
310 | name = Unicode(name='name', allow_none=False) |
311 | @@ -393,13 +399,21 @@ |
312 | @property |
313 | def available_processors(self): |
314 | """See `ISnap`.""" |
315 | - processors = Store.of(self).find( |
316 | - Processor, |
317 | - Processor.id == DistroArchSeries.processor_id, |
318 | - DistroArchSeries.id.is_in( |
319 | + clauses = [Processor.id == DistroArchSeries.processor_id] |
320 | + if self.distro_series is not None: |
321 | + clauses.append(DistroArchSeries.id.is_in( |
322 | self.distro_series.enabled_architectures.get_select_expr( |
323 | DistroArchSeries.id))) |
324 | - return processors.config(distinct=True) |
325 | + else: |
326 | + # We don't know the series until we've looked at snapcraft.yaml |
327 | + # to dispatch a build, so enabled architectures for any active |
328 | + # series will do. |
329 | + clauses.extend([ |
330 | + DistroArchSeries.enabled, |
331 | + DistroArchSeries.distroseriesID == DistroSeries.id, |
332 | + DistroSeries.status.is_in(ACTIVE_STATUSES), |
333 | + ]) |
334 | + return Store.of(self).find(Processor, *clauses).config(distinct=True) |
335 | |
336 | def _getProcessors(self): |
337 | return list(Store.of(self).find( |
338 | @@ -445,16 +459,32 @@ |
339 | |
340 | processors = property(_getProcessors, setProcessors) |
341 | |
342 | - def getAllowedArchitectures(self): |
343 | + def _isBuildableArchitectureAllowed(self, das): |
344 | + """Check whether we may build for a buildable `DistroArchSeries`. |
345 | + |
346 | + The caller is assumed to have already checked that a suitable chroot |
347 | + is available (either directly or via |
348 | + `DistroSeries.buildable_architectures`). |
349 | + """ |
350 | + return ( |
351 | + das.enabled |
352 | + and das.processor in self.processors |
353 | + and ( |
354 | + das.processor.supports_virtualized |
355 | + or not self.require_virtualized)) |
356 | + |
357 | + def _isArchitectureAllowed(self, das, pocket): |
358 | + return ( |
359 | + das.getChroot(pocket=pocket) is not None |
360 | + and self._isBuildableArchitectureAllowed(das)) |
361 | + |
362 | + def getAllowedArchitectures(self, distro_series=None): |
363 | """See `ISnap`.""" |
364 | + if distro_series is None: |
365 | + distro_series = self.distro_series |
366 | return [ |
367 | - das for das in self.distro_series.buildable_architectures |
368 | - if ( |
369 | - das.enabled |
370 | - and das.processor in self.processors |
371 | - and ( |
372 | - das.processor.supports_virtualized |
373 | - or not self.require_virtualized))] |
374 | + das for das in distro_series.buildable_architectures |
375 | + if self._isBuildableArchitectureAllowed(das)] |
376 | |
377 | @property |
378 | def store_distro_series(self): |
379 | @@ -559,8 +589,8 @@ |
380 | channels=None, build_request=None): |
381 | """See `ISnap`.""" |
382 | self._checkRequestBuild(requester, archive) |
383 | - if distro_arch_series not in self.getAllowedArchitectures(): |
384 | - raise SnapBuildDisallowedArchitecture(distro_arch_series) |
385 | + if not self._isArchitectureAllowed(distro_arch_series, pocket): |
386 | + raise SnapBuildDisallowedArchitecture(distro_arch_series, pocket) |
387 | |
388 | pending = IStore(self).find( |
389 | SnapBuild, |
390 | @@ -587,10 +617,46 @@ |
391 | self, requester, archive, pocket, channels) |
392 | return self.getBuildRequest(job.job_id) |
393 | |
394 | + @staticmethod |
395 | + def _findBase(snapcraft_data): |
396 | + """Find a suitable base for a build.""" |
397 | + snap_base_set = getUtility(ISnapBaseSet) |
398 | + if "base" in snapcraft_data: |
399 | + snap_base_name = snapcraft_data["base"] |
400 | + if isinstance(snap_base_name, bytes): |
401 | + snap_base_name = snap_base_name.decode("UTF-8") |
402 | + return snap_base_set.getByName(snap_base_name) |
403 | + else: |
404 | + return snap_base_set.getDefault() |
405 | + |
406 | + def _pickDistroSeries(self, snap_base, snapcraft_data): |
407 | + """Pick a suitable `IDistroSeries` for a build.""" |
408 | + if snap_base is not None: |
409 | + return self.distro_series or snap_base.distro_series |
410 | + elif self.distro_series is None: |
411 | + # A base is mandatory if there's no configured distro series. |
412 | + raise NoSuchSnapBase(snapcraft_data.get("base", "<default>")) |
413 | + else: |
414 | + return self.distro_series |
415 | + |
416 | + def _pickChannels(self, snap_base, channels=None): |
417 | + """Pick suitable snap channels for a build.""" |
418 | + if snap_base is not None: |
419 | + new_channels = dict(snap_base.build_channels) |
420 | + if channels is not None: |
421 | + new_channels.update(channels) |
422 | + return new_channels |
423 | + else: |
424 | + return channels |
425 | + |
426 | def requestBuildsFromJob(self, requester, archive, pocket, channels=None, |
427 | allow_failures=False, fetch_snapcraft_yaml=True, |
428 | build_request=None, logger=None): |
429 | """See `ISnap`.""" |
430 | + if not fetch_snapcraft_yaml and self.distro_series is None: |
431 | + # Slightly misleading, but requestAutoBuilds is the only place |
432 | + # this can happen right now and it raises a more specific error. |
433 | + raise CannotRequestAutoBuilds("distro_series") |
434 | try: |
435 | if fetch_snapcraft_yaml: |
436 | try: |
437 | @@ -606,12 +672,20 @@ |
438 | snapcraft_data = {} |
439 | else: |
440 | snapcraft_data = {} |
441 | + |
442 | + # Find a suitable SnapBase, and combine it with other |
443 | + # configuration to find a suitable distro series and suitable |
444 | + # channels. |
445 | + snap_base = self._findBase(snapcraft_data) |
446 | + distro_series = self._pickDistroSeries(snap_base, snapcraft_data) |
447 | + channels = self._pickChannels(snap_base, channels=channels) |
448 | + |
449 | # Sort by Processor.id for determinism. This is chosen to be |
450 | # the same order as in BinaryPackageBuildSet.createForSource, to |
451 | # minimise confusion. |
452 | supported_arches = OrderedDict( |
453 | (das.architecturetag, das) for das in sorted( |
454 | - self.getAllowedArchitectures(), |
455 | + self.getAllowedArchitectures(distro_series), |
456 | key=attrgetter("processor.id"))) |
457 | architectures_to_build = determine_architectures_to_build( |
458 | snapcraft_data, supported_arches.keys()) |
459 | @@ -652,6 +726,11 @@ |
460 | raise CannotRequestAutoBuilds("auto_build_archive") |
461 | if self.auto_build_pocket is None: |
462 | raise CannotRequestAutoBuilds("auto_build_pocket") |
463 | + if self.distro_series is None: |
464 | + raise IncompatibleArguments( |
465 | + "Cannot use requestAutoBuilds for a snap package without " |
466 | + "distro_series being set. Consider using requestBuilds " |
467 | + "instead.") |
468 | self.is_stale = False |
469 | if logger is not None: |
470 | logger.debug( |
471 | @@ -690,7 +769,7 @@ |
472 | query_args = [ |
473 | SnapBuild.snap == self, |
474 | SnapBuild.archive_id == Archive.id, |
475 | - Archive._enabled == True, |
476 | + Archive._enabled, |
477 | get_enabled_archive_filter( |
478 | getUtility(ILaunchBag).user, include_public=True, |
479 | include_subscribed=True) |
480 | @@ -1223,8 +1302,8 @@ |
481 | ] |
482 | return IStore(Snap).using(*origin).find( |
483 | Snap, |
484 | - Snap.is_stale == True, |
485 | - Snap.auto_build == True, |
486 | + Snap.is_stale, |
487 | + Snap.auto_build, |
488 | SnapBuild.date_created == None).config(distinct=True) |
489 | |
490 | @classmethod |
491 | |
492 | === modified file 'lib/lp/snappy/templates/snap-index.pt' |
493 | --- lib/lp/snappy/templates/snap-index.pt 2018-09-27 13:51:42 +0000 |
494 | +++ lib/lp/snappy/templates/snap-index.pt 2019-02-18 14:51:25 +0000 |
495 | @@ -45,9 +45,11 @@ |
496 | <dt>Owner:</dt> |
497 | <dd tal:content="structure view/person_picker"/> |
498 | </dl> |
499 | - <dl id="distro_series"> |
500 | + <dl id="distro_series" |
501 | + tal:define="distro_series context/distro_series" |
502 | + tal:condition="distro_series"> |
503 | <dt>Distribution series:</dt> |
504 | - <dd tal:define="distro_series context/distro_series"> |
505 | + <dd> |
506 | <a tal:attributes="href distro_series/fmt:url" |
507 | tal:content="distro_series/fullseriesname"/> |
508 | <a tal:replace="structure view/menu:overview/edit/fmt:icon"/> |
509 | |
510 | === modified file 'lib/lp/snappy/templates/snap-request-builds.pt' |
511 | --- lib/lp/snappy/templates/snap-request-builds.pt 2015-09-18 13:32:09 +0000 |
512 | +++ lib/lp/snappy/templates/snap-request-builds.pt 2019-02-18 14:51:25 +0000 |
513 | @@ -14,9 +14,17 @@ |
514 | <tal:widget define="widget nocall:view/widgets/archive"> |
515 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
516 | </tal:widget> |
517 | - <tal:widget define="widget nocall:view/widgets/distro_arch_series"> |
518 | - <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
519 | - </tal:widget> |
520 | + <tal:comment condition="nothing"> |
521 | + XXX cjwatson 2019-02-05: Architecture selection is still useful |
522 | + even when the series is inferred from snapcraft.yaml, but it's |
523 | + more difficult to work out what choices to offer. For now we just |
524 | + make it all-or-nothing in that case. |
525 | + </tal:comment> |
526 | + <tal:has-distro-series condition="context/distro_series"> |
527 | + <tal:widget define="widget nocall:view/widgets/distro_arch_series"> |
528 | + <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
529 | + </tal:widget> |
530 | + </tal:has-distro-series> |
531 | <tal:widget define="widget nocall:view/widgets/pocket"> |
532 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
533 | </tal:widget> |
534 | |
535 | === modified file 'lib/lp/snappy/tests/test_snap.py' |
536 | --- lib/lp/snappy/tests/test_snap.py 2019-02-05 10:44:18 +0000 |
537 | +++ lib/lp/snappy/tests/test_snap.py 2019-02-18 14:51:25 +0000 |
538 | @@ -62,6 +62,7 @@ |
539 | from lp.registry.enums import PersonVisibility |
540 | from lp.registry.interfaces.distribution import IDistributionSet |
541 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
542 | +from lp.registry.interfaces.series import SeriesStatus |
543 | from lp.services.config import config |
544 | from lp.services.database.constants import ( |
545 | ONE_DAY_AGO, |
546 | @@ -100,6 +101,10 @@ |
547 | SnapPrivacyMismatch, |
548 | SnapPrivateFeatureDisabled, |
549 | ) |
550 | +from lp.snappy.interfaces.snapbase import ( |
551 | + ISnapBaseSet, |
552 | + NoSuchSnapBase, |
553 | + ) |
554 | from lp.snappy.interfaces.snapbuild import ( |
555 | ISnapBuild, |
556 | ISnapBuildSet, |
557 | @@ -107,7 +112,10 @@ |
558 | from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource |
559 | from lp.snappy.interfaces.snapjob import ISnapRequestBuildsJobSource |
560 | from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient |
561 | -from lp.snappy.model.snap import SnapSet |
562 | +from lp.snappy.model.snap import ( |
563 | + Snap, |
564 | + SnapSet, |
565 | + ) |
566 | from lp.snappy.model.snapbuild import SnapFile |
567 | from lp.snappy.model.snapbuildjob import SnapBuildJob |
568 | from lp.snappy.model.snapjob import SnapJob |
569 | @@ -326,6 +334,22 @@ |
570 | SnapBuildDisallowedArchitecture, snap.requestBuild, |
571 | snap.owner, snap.distro_series.main_archive, arches[1], |
572 | PackagePublishingPocket.UPDATES) |
573 | + inactive_proc = self.factory.makeProcessor(supports_virtualized=True) |
574 | + inactive_arch = self.makeBuildableDistroArchSeries( |
575 | + distroseries=self.factory.makeDistroSeries( |
576 | + status=SeriesStatus.OBSOLETE), |
577 | + processor=inactive_proc) |
578 | + snap_no_ds = self.factory.makeSnap(distroseries=None) |
579 | + snap_no_ds.requestBuild( |
580 | + snap_no_ds.owner, distroseries.main_archive, arches[0], |
581 | + PackagePublishingPocket.UPDATES) |
582 | + snap_no_ds.requestBuild( |
583 | + snap_no_ds.owner, distroseries.main_archive, arches[1], |
584 | + PackagePublishingPocket.UPDATES) |
585 | + self.assertRaises( |
586 | + SnapBuildDisallowedArchitecture, snap.requestBuild, |
587 | + snap.owner, snap.distro_series.main_archive, inactive_arch, |
588 | + PackagePublishingPocket.UPDATES) |
589 | |
590 | def test_requestBuild_virtualization(self): |
591 | # New builds are virtualized if any of the processor, snap or |
592 | @@ -439,6 +463,56 @@ |
593 | pocket=Equals(PackagePublishingPocket.UPDATES), |
594 | channels=Equals({"snapcraft": "edge"}))) |
595 | |
596 | + def test_requestBuilds_without_distroseries(self): |
597 | + # requestBuilds schedules a job for a snap without a distroseries. |
598 | + snap = self.factory.makeSnap(distroseries=None) |
599 | + archive = self.factory.makeArchive() |
600 | + now = get_transaction_timestamp(IStore(snap)) |
601 | + with person_logged_in(snap.owner.teamowner): |
602 | + request = snap.requestBuilds( |
603 | + snap.owner.teamowner, archive, PackagePublishingPocket.UPDATES, |
604 | + channels={"snapcraft": "edge"}) |
605 | + self.assertThat(request, MatchesStructure( |
606 | + date_requested=Equals(now), |
607 | + date_finished=Is(None), |
608 | + snap=Equals(snap), |
609 | + status=Equals(SnapBuildRequestStatus.PENDING), |
610 | + error_message=Is(None), |
611 | + builds=AfterPreprocessing(set, MatchesSetwise()), |
612 | + archive=Equals(archive))) |
613 | + [job] = getUtility(ISnapRequestBuildsJobSource).iterReady() |
614 | + self.assertThat(job, MatchesStructure( |
615 | + job_id=Equals(request.id), |
616 | + job=MatchesStructure.byEquality(status=JobStatus.WAITING), |
617 | + snap=Equals(snap), |
618 | + requester=Equals(snap.owner.teamowner), |
619 | + archive=Equals(archive), |
620 | + pocket=Equals(PackagePublishingPocket.UPDATES), |
621 | + channels=Equals({"snapcraft": "edge"}))) |
622 | + |
623 | + def test__findBase(self): |
624 | + snap_base_set = getUtility(ISnapBaseSet) |
625 | + with admin_logged_in(): |
626 | + snap_bases = [self.factory.makeSnapBase() for _ in range(2)] |
627 | + for snap_base in snap_bases: |
628 | + self.assertEqual( |
629 | + snap_base, |
630 | + Snap._findBase({"base": snap_base.name})) |
631 | + self.assertRaises( |
632 | + NoSuchSnapBase, Snap._findBase, |
633 | + {"base": "nonexistent"}) |
634 | + self.assertIsNone(Snap._findBase({})) |
635 | + with admin_logged_in(): |
636 | + snap_base_set.setDefault(snap_bases[0]) |
637 | + for snap_base in snap_bases: |
638 | + self.assertEqual( |
639 | + snap_base, |
640 | + Snap._findBase({"base": snap_base.name})) |
641 | + self.assertRaises( |
642 | + NoSuchSnapBase, Snap._findBase, |
643 | + {"base": "nonexistent"}) |
644 | + self.assertEqual(snap_bases[0], Snap._findBase({})) |
645 | + |
646 | def makeRequestBuildsJob(self, arch_tags, git_ref=None): |
647 | distro = self.factory.makeDistribution() |
648 | distroseries = self.factory.makeDistroSeries(distribution=distro) |
649 | @@ -447,11 +521,9 @@ |
650 | name=arch_tag, supports_virtualized=True) |
651 | for arch_tag in arch_tags] |
652 | for processor in processors: |
653 | - das = self.factory.makeDistroArchSeries( |
654 | + self.makeBuildableDistroArchSeries( |
655 | distroseries=distroseries, architecturetag=processor.name, |
656 | processor=processor) |
657 | - das.addOrUpdateChroot(self.factory.makeLibraryFileAlias( |
658 | - filename="fake_chroot.tar.gz", db_only=True)) |
659 | if git_ref is None: |
660 | [git_ref] = self.factory.makeGitRefs() |
661 | snap = self.factory.makeSnap( |
662 | @@ -460,15 +532,18 @@ |
663 | snap, snap.owner.teamowner, distro.main_archive, |
664 | PackagePublishingPocket.RELEASE, {"snapcraft": "edge"}) |
665 | |
666 | - def assertRequestedBuildsMatch(self, builds, job, arch_tags): |
667 | + def assertRequestedBuildsMatch(self, builds, job, arch_tags, channels, |
668 | + distro_series=None): |
669 | + if distro_series is None: |
670 | + distro_series = job.snap.distro_series |
671 | self.assertThat(builds, MatchesSetwise( |
672 | *(MatchesStructure( |
673 | requester=Equals(job.requester), |
674 | snap=Equals(job.snap), |
675 | archive=Equals(job.archive), |
676 | - distro_arch_series=Equals(job.snap.distro_series[arch_tag]), |
677 | + distro_arch_series=Equals(distro_series[arch_tag]), |
678 | pocket=Equals(job.pocket), |
679 | - channels=Equals(job.channels)) |
680 | + channels=Equals(channels)) |
681 | for arch_tag in arch_tags))) |
682 | |
683 | def test_requestBuildsFromJob_restricts_explicit_list(self): |
684 | @@ -489,7 +564,7 @@ |
685 | job.requester, job.archive, job.pocket, |
686 | removeSecurityProxy(job.channels), |
687 | build_request=job.build_request) |
688 | - self.assertRequestedBuildsMatch(builds, job, ["sparc"]) |
689 | + self.assertRequestedBuildsMatch(builds, job, ["sparc"], job.channels) |
690 | |
691 | def test_requestBuildsFromJob_no_explicit_architectures(self): |
692 | # If the snap doesn't specify any architectures, |
693 | @@ -505,7 +580,89 @@ |
694 | job.requester, job.archive, job.pocket, |
695 | removeSecurityProxy(job.channels), |
696 | build_request=job.build_request) |
697 | - self.assertRequestedBuildsMatch(builds, job, ["mips64el", "riscv64"]) |
698 | + self.assertRequestedBuildsMatch( |
699 | + builds, job, ["mips64el", "riscv64"], job.channels) |
700 | + |
701 | + def test_requestBuildsFromJob_no_distroseries_explicit_base(self): |
702 | + # If the snap doesn't specify a distroseries but has an explicit |
703 | + # base, requestBuildsFromJob requests builds for the appropriate |
704 | + # distroseries for the base. |
705 | + self.useFixture(GitHostingFixture(blob="base: test-base\n")) |
706 | + with admin_logged_in(): |
707 | + snap_base = self.factory.makeSnapBase( |
708 | + name="test-base", |
709 | + build_channels={"snapcraft": "stable/launchpad-buildd"}) |
710 | + self.factory.makeSnapBase() |
711 | + for arch_tag in ("mips64el", "riscv64"): |
712 | + self.makeBuildableDistroArchSeries( |
713 | + distroseries=snap_base.distro_series, architecturetag=arch_tag, |
714 | + processor=self.factory.makeProcessor( |
715 | + name=arch_tag, supports_virtualized=True)) |
716 | + snap = self.factory.makeSnap( |
717 | + distroseries=None, git_ref=self.factory.makeGitRefs()[0]) |
718 | + job = getUtility(ISnapRequestBuildsJobSource).create( |
719 | + snap, snap.owner.teamowner, snap_base.distro_series.main_archive, |
720 | + PackagePublishingPocket.RELEASE, None) |
721 | + self.assertEqual( |
722 | + get_transaction_timestamp(IStore(snap)), job.date_created) |
723 | + transaction.commit() |
724 | + with person_logged_in(job.requester): |
725 | + builds = snap.requestBuildsFromJob( |
726 | + job.requester, job.archive, job.pocket, |
727 | + build_request=job.build_request) |
728 | + self.assertRequestedBuildsMatch( |
729 | + builds, job, ["mips64el", "riscv64"], snap_base.build_channels, |
730 | + distro_series=snap_base.distro_series) |
731 | + |
732 | + def test_requestBuildsFromJob_no_distroseries_no_explicit_base(self): |
733 | + # If the snap doesn't specify a distroseries and has no explicit |
734 | + # base, requestBuildsFromJob requests builds for the appropriate |
735 | + # distroseries for the default base. |
736 | + self.useFixture(GitHostingFixture(blob="name: foo\n")) |
737 | + with admin_logged_in(): |
738 | + snap_base = self.factory.makeSnapBase( |
739 | + build_channels={"snapcraft": "stable/launchpad-buildd"}) |
740 | + getUtility(ISnapBaseSet).setDefault(snap_base) |
741 | + self.factory.makeSnapBase() |
742 | + for arch_tag in ("mips64el", "riscv64"): |
743 | + self.makeBuildableDistroArchSeries( |
744 | + distroseries=snap_base.distro_series, architecturetag=arch_tag, |
745 | + processor=self.factory.makeProcessor( |
746 | + name=arch_tag, supports_virtualized=True)) |
747 | + snap = self.factory.makeSnap( |
748 | + distroseries=None, git_ref=self.factory.makeGitRefs()[0]) |
749 | + job = getUtility(ISnapRequestBuildsJobSource).create( |
750 | + snap, snap.owner.teamowner, snap_base.distro_series.main_archive, |
751 | + PackagePublishingPocket.RELEASE, None) |
752 | + self.assertEqual( |
753 | + get_transaction_timestamp(IStore(snap)), job.date_created) |
754 | + transaction.commit() |
755 | + with person_logged_in(job.requester): |
756 | + builds = snap.requestBuildsFromJob( |
757 | + job.requester, job.archive, job.pocket, |
758 | + build_request=job.build_request) |
759 | + self.assertRequestedBuildsMatch( |
760 | + builds, job, ["mips64el", "riscv64"], snap_base.build_channels, |
761 | + distro_series=snap_base.distro_series) |
762 | + |
763 | + def test_requestBuildsFromJob_no_distroseries_no_default_base(self): |
764 | + # If the snap doesn't specify a distroseries and has an explicit |
765 | + # base, and there is no default base, requestBuildsFromJob gives up. |
766 | + self.useFixture(GitHostingFixture(blob="name: foo\n")) |
767 | + with admin_logged_in(): |
768 | + snap_base = self.factory.makeSnapBase( |
769 | + build_channels={"snapcraft": "stable/launchpad-buildd"}) |
770 | + snap = self.factory.makeSnap( |
771 | + distroseries=None, git_ref=self.factory.makeGitRefs()[0]) |
772 | + job = getUtility(ISnapRequestBuildsJobSource).create( |
773 | + snap, snap.owner.teamowner, snap_base.distro_series.main_archive, |
774 | + PackagePublishingPocket.RELEASE, None) |
775 | + transaction.commit() |
776 | + with person_logged_in(job.requester): |
777 | + self.assertRaises( |
778 | + NoSuchSnapBase, snap.requestBuildsFromJob, |
779 | + job.requester, job.archive, job.pocket, |
780 | + build_request=job.build_request) |
781 | |
782 | def test_requestBuildsFromJob_unsupported_remote(self): |
783 | # If the snap is based on an external Git repository from which we |
784 | @@ -523,7 +680,8 @@ |
785 | job.requester, job.archive, job.pocket, |
786 | removeSecurityProxy(job.channels), |
787 | build_request=job.build_request) |
788 | - self.assertRequestedBuildsMatch(builds, job, ["mips64el", "riscv64"]) |
789 | + self.assertRequestedBuildsMatch( |
790 | + builds, job, ["mips64el", "riscv64"], job.channels) |
791 | |
792 | def test_requestBuildsFromJob_triggers_webhooks(self): |
793 | # requestBuildsFromJob triggers webhooks, and the payload includes a |
794 | @@ -1911,6 +2069,29 @@ |
795 | self.arm = self.factory.makeProcessor( |
796 | name="arm", restricted=True, build_by_default=False) |
797 | |
798 | + def test_available_processors_with_distro_series(self): |
799 | + # If the snap has a distroseries, only those processors that are |
800 | + # enabled for that series are available. |
801 | + distroseries = self.factory.makeDistroSeries() |
802 | + for processor in self.default_procs: |
803 | + self.factory.makeDistroArchSeries( |
804 | + distroseries=distroseries, architecturetag=processor.name, |
805 | + processor=processor) |
806 | + self.factory.makeDistroArchSeries( |
807 | + architecturetag=self.arm.name, processor=self.arm) |
808 | + snap = self.factory.makeSnap(distroseries=distroseries) |
809 | + self.assertContentEqual(self.default_procs, snap.available_processors) |
810 | + |
811 | + def test_available_processors_without_distro_series(self): |
812 | + # If the snap does not have a distroseries, then processors that are |
813 | + # enabled for any active series are available. |
814 | + snap = self.factory.makeSnap(distroseries=None) |
815 | + # 386 and hppa have corresponding DASes in sampledata for active |
816 | + # distroseries. |
817 | + self.assertContentEqual( |
818 | + ["386", "hppa"], |
819 | + [processor.name for processor in snap.available_processors]) |
820 | + |
821 | def test_new_default_processors(self): |
822 | # SnapSet.new creates a SnapArch for each available Processor with |
823 | # build_by_default set. |
824 | |
825 | === modified file 'lib/lp/testing/factory.py' |
826 | --- lib/lp/testing/factory.py 2019-02-18 14:51:25 +0000 |
827 | +++ lib/lp/testing/factory.py 2019-02-18 14:51:25 +0000 |
828 | @@ -4711,7 +4711,7 @@ |
829 | target, self.makePerson(), delivery_url, event_types or [], |
830 | active, secret) |
831 | |
832 | - def makeSnap(self, registrant=None, owner=None, distroseries=None, |
833 | + def makeSnap(self, registrant=None, owner=None, distroseries=_DEFAULT, |
834 | name=None, branch=None, git_ref=None, auto_build=False, |
835 | auto_build_archive=None, auto_build_pocket=None, |
836 | auto_build_channels=None, is_stale=None, |
837 | @@ -4725,7 +4725,7 @@ |
838 | registrant = self.makePerson() |
839 | if owner is None: |
840 | owner = self.makeTeam(registrant) |
841 | - if distroseries is None: |
842 | + if distroseries is _DEFAULT: |
843 | distroseries = self.makeDistroSeries() |
844 | if name is None: |
845 | name = self.getUniqueString(u"snap-name") |
846 | @@ -4785,7 +4785,7 @@ |
847 | distroseries = self.makeDistroSeries( |
848 | distribution=archive.distribution) |
849 | else: |
850 | - distroseries = None |
851 | + distroseries = _DEFAULT |
852 | if registrant is None: |
853 | registrant = requester |
854 | snap = self.makeSnap( |