Merge lp:~cjwatson/launchpad/snap-without-distro-series into lp:launchpad

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
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(