← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-without-distro-series into lp:launchpad with lp:~cjwatson/launchpad/base-snap as a prerequisite.

Commit message:
Support building snaps without a configured distro series.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1812985 in Launchpad itself: "Support snapcraft base snaps"
  https://bugs.launchpad.net/launchpad/+bug/1812985

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-without-distro-series/+merge/362730

In this case, the series is inferred from the base snap given by snapcraft.yaml, if any.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-without-distro-series into lp:launchpad.
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py	2019-01-30 12:28:49 +0000
+++ lib/lp/snappy/browser/snap.py	2019-02-05 12:46:08 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Snap views."""
@@ -44,6 +44,7 @@
 from lp.app.browser.tales import format_link
 from lp.app.enums import PRIVATE_INFORMATION_TYPES
 from lp.app.interfaces.informationtype import IInformationType
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.widgets.itemswidgets import (
     LabeledMultiCheckBoxWidget,
     LaunchpadDropdownWidget,
@@ -310,7 +311,13 @@
     def initial_values(self):
         """See `LaunchpadFormView`."""
         return {
-            'archive': self.context.distro_series.main_archive,
+            'archive': (
+                # XXX cjwatson 2019-02-04: In order to support non-Ubuntu
+                # bases, we'd need to store this as None and guess it based
+                # on the guessed distro series; but this will do for now.
+                getUtility(ILaunchpadCelebrities).ubuntu.main_archive
+                if self.context.distro_series is None
+                else self.context.distro_series.main_archive),
             'distro_arch_series': [],
             'pocket': PackagePublishingPocket.UPDATES,
             }
@@ -341,7 +348,7 @@
 
     @action('Request builds', name='request')
     def request_action(self, action, data):
-        if data['distro_arch_series']:
+        if data.get('distro_arch_series', []):
             builds, informational = self.requestBuild(data)
             already_pending = informational.get('already_pending')
             notification_text = new_builds_notification_text(
@@ -498,7 +505,13 @@
             'processors': [
                 p for p in getUtility(IProcessorSet).getAll()
                 if p.build_by_default],
-            'auto_build_archive': distro_series.main_archive,
+            'auto_build_archive': (
+                # XXX cjwatson 2019-02-04: In order to support non-Ubuntu
+                # bases, we'd need to store this as None and guess it based
+                # on the guessed distro series; but this will do for now.
+                getUtility(ILaunchpadCelebrities).ubuntu.main_archive
+                if distro_series is None
+                else distro_series.main_archive),
             'auto_build_pocket': PackagePublishingPocket.UPDATES,
             }
 

=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py	2019-01-30 12:28:49 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py	2019-02-05 12:46:08 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test snap package views."""
@@ -1173,11 +1173,13 @@
         self.factory.makeBuilder(virtualized=True)
 
     def makeSnap(self, **kwargs):
+        if "distroseries" not in kwargs:
+            kwargs["distroseries"] = self.distroseries
         if kwargs.get("branch") is None and kwargs.get("git_ref") is None:
             kwargs["branch"] = self.factory.makeAnyBranch()
         return self.factory.makeSnap(
-            registrant=self.person, owner=self.person,
-            distroseries=self.distroseries, name="snap-name", **kwargs)
+            registrant=self.person, owner=self.person, name="snap-name",
+            **kwargs)
 
     def makeBuild(self, snap=None, archive=None, date_created=None, **kwargs):
         if snap is None:
@@ -1299,6 +1301,15 @@
             Primary Archive for Ubuntu Linux
             """, self.getMainText(build.snap))
 
+    def test_index_no_distro_series(self):
+        # If the snap is configured to guess an appropriate distro series
+        # from snapcraft.yaml, then the index page does not show a distro
+        # series.
+        snap = self.makeSnap(distroseries=None)
+        text = self.getMainText(snap)
+        self.assertIn("Snap package information", text)
+        self.assertNotIn("Distribution series:", text)
+
     def test_index_success_with_buildlog(self):
         # The build log is shown if it is there.
         build = self.makeBuild(
@@ -1383,6 +1394,8 @@
         store_upload_tag = soupmatchers.Tag(
             "store upload", "div", attrs={"id": "store_upload"})
         self.assertThat(view(), soupmatchers.HTMLContains(
+            soupmatchers.Tag(
+                "distribution series", "dl", attrs={"id": "distro_series"}),
             soupmatchers.Within(
                 store_upload_tag,
                 soupmatchers.Tag(
@@ -1629,3 +1642,25 @@
         login_person(self.person)
         [request] = self.snap.pending_build_requests
         self.assertEqual(ppa, request.archive)
+
+    def test_request_builds_no_distro_series(self):
+        # Requesting builds of a snap configured to guess an appropriate
+        # distro series from snapcraft.yaml creates a pending build request.
+        login_person(self.person)
+        self.snap.distro_series = None
+        browser = self.getViewBrowser(
+            self.snap, "+request-builds", user=self.person)
+        browser.getControl("Request builds").click()
+
+        login_person(self.person)
+        [request] = self.snap.pending_build_requests
+        self.assertThat(removeSecurityProxy(request), MatchesStructure(
+            snap=Equals(self.snap),
+            status=Equals(SnapBuildRequestStatus.PENDING),
+            error_message=Is(None),
+            builds=AfterPreprocessing(list, Equals([])),
+            archive=Equals(self.ubuntu.main_archive),
+            _job=MatchesStructure(
+                requester=Equals(self.person),
+                pocket=Equals(PackagePublishingPocket.UPDATES),
+                channels=Is(None))))

=== modified file 'lib/lp/snappy/browser/widgets/snaparchive.py'
--- lib/lp/snappy/browser/widgets/snaparchive.py	2016-06-20 20:47:20 +0000
+++ lib/lp/snappy/browser/widgets/snaparchive.py	2019-02-05 12:46:08 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -70,7 +70,8 @@
 
     @property
     def main_archive(self):
-        if ISnap.providedBy(self.context.context):
+        if (ISnap.providedBy(self.context.context) and
+                self.context.context.distro_series is not None):
             return self.context.context.distro_series.main_archive
         else:
             return getUtility(ILaunchpadCelebrities).ubuntu.main_archive

=== modified file 'lib/lp/snappy/browser/widgets/tests/test_snaparchivewidget.py'
--- lib/lp/snappy/browser/widgets/tests/test_snaparchivewidget.py	2017-11-10 11:28:43 +0000
+++ lib/lp/snappy/browser/widgets/tests/test_snaparchivewidget.py	2019-02-05 12:46:08 +0000
@@ -1,10 +1,11 @@
-# Copyright 2015-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 __metaclass__ = type
 
+from functools import partial
 import re
 
 from lazr.restful.fields import Reference
@@ -36,8 +37,10 @@
 from lp.testing.layers import DatabaseFunctionalLayer
 
 
-def make_snap(test_case):
-    return test_case.factory.makeSnap(distroseries=test_case.distroseries)
+def make_snap(test_case, **kwargs):
+    if "distroseries" not in kwargs:
+        kwargs["distroseries"] = test_case.distroseries
+    return test_case.factory.makeSnap(**kwargs)
 
 
 def make_branch(test_case):
@@ -54,6 +57,8 @@
 
     scenarios = [
         ("Snap", {"context_factory": make_snap}),
+        ("Snap with no distroseries",
+         {"context_factory": partial(make_snap, distroseries=None)}),
         ("Branch", {"context_factory": make_branch}),
         ("GitRepository", {"context_factory": make_git_repository}),
         ]
@@ -219,12 +224,13 @@
 
     def test_getInputValue_primary(self):
         # When the primary radio button is selected, the field value is the
-        # context's primary archive if the context is a Snap, or the Ubuntu
-        # primary archive otherwise.
+        # context's primary archive if the context is a Snap and has a
+        # distroseries, or the Ubuntu primary archive otherwise.
         self.widget.request = LaunchpadTestRequest(
             form={"field.archive": "primary"})
-        if ISnap.providedBy(self.context):
-            expected_main_archive = self.distroseries.main_archive
+        if (ISnap.providedBy(self.context) and
+                self.context.distro_series is not None):
+            expected_main_archive = self.context.distro_series.main_archive
         else:
             expected_main_archive = (
                 getUtility(ILaunchpadCelebrities).ubuntu.main_archive)

=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py	2018-11-07 18:12:08 +0000
+++ lib/lp/snappy/interfaces/snap.py	2019-02-05 12:46:08 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2019 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Snap package interfaces."""
@@ -153,10 +153,10 @@
 class SnapBuildDisallowedArchitecture(Exception):
     """A build was requested for a disallowed architecture."""
 
-    def __init__(self, das):
+    def __init__(self, das, pocket):
         super(SnapBuildDisallowedArchitecture, self).__init__(
-            "This snap package is not allowed to build for %s." %
-            das.displayname)
+            "This snap package is not allowed to build for %s/%s." %
+            (das.distroseries.getSuite(pocket), das.architecturetag))
 
 
 @error_status(httplib.UNAUTHORIZED)
@@ -453,6 +453,8 @@
         :param build_request: The `ISnapBuildRequest` job being processed,
             if any.
         :param logger: An optional logger.
+        :raises CannotRequestAutoBuilds: if fetch_snapcraft_yaml is False
+            and self.distro_series is not set.
         :return: A sequence of `ISnapBuild` instances.
         """
 
@@ -547,6 +549,10 @@
                           logger=None):
         """Create and return automatic builds for this snap package.
 
+        This webservice API method is deprecated.  It is normally better to
+        use the `requestBuilds` method instead, which can make dispatching
+        decisions based on the contents of snapcraft.yaml.
+
         :param allow_failures: If True, log exceptions other than "already
             pending" from individual build requests; if False, raise them to
             the caller.
@@ -557,6 +563,7 @@
         :param logger: An optional logger.
         :raises CannotRequestAutoBuilds: if no auto_build_archive or
             auto_build_pocket is set.
+        :raises IncompatibleArguments: if no distro_series is set.
         :return: A sequence of `ISnapBuild` instances.
         """
 
@@ -632,9 +639,11 @@
         description=_("The owner of this snap package.")))
 
     distro_series = exported(Reference(
-        IDistroSeries, title=_("Distro Series"), required=True, readonly=False,
+        IDistroSeries, title=_("Distro Series"),
+        required=False, readonly=False,
         description=_(
-            "The series for which the snap package should be built.")))
+            "The series for which the snap package should be built.  If not "
+            "set, Launchpad will guess at an appropriate series.")))
 
     name = exported(TextLine(
         title=_("Name"), required=True, readonly=False,

=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py	2019-02-05 12:46:08 +0000
+++ lib/lp/snappy/model/snap.py	2019-02-05 12:46:08 +0000
@@ -93,6 +93,8 @@
     IHasOwner,
     IPersonRoles,
     )
+from lp.registry.model.distroseries import DistroSeries
+from lp.registry.model.series import ACTIVE_STATUSES
 from lp.registry.model.teammembership import TeamParticipation
 from lp.services.config import config
 from lp.services.database.bulk import load_related
@@ -129,6 +131,10 @@
 from lp.services.webhooks.interfaces import IWebhookSet
 from lp.services.webhooks.model import WebhookTargetMixin
 from lp.snappy.adapters.buildarch import determine_architectures_to_build
+from lp.snappy.interfaces.basesnap import (
+    IBaseSnapSet,
+    NoSuchBaseSnap,
+    )
 from lp.snappy.interfaces.snap import (
     BadSnapSearchContext,
     BadSnapSource,
@@ -258,7 +264,7 @@
     owner_id = Int(name='owner', allow_none=False)
     owner = Reference(owner_id, 'Person.id')
 
-    distro_series_id = Int(name='distro_series', allow_none=False)
+    distro_series_id = Int(name='distro_series', allow_none=True)
     distro_series = Reference(distro_series_id, 'DistroSeries.id')
 
     name = Unicode(name='name', allow_none=False)
@@ -393,13 +399,21 @@
     @property
     def available_processors(self):
         """See `ISnap`."""
-        processors = Store.of(self).find(
-            Processor,
-            Processor.id == DistroArchSeries.processor_id,
-            DistroArchSeries.id.is_in(
+        clauses = [Processor.id == DistroArchSeries.processor_id]
+        if self.distro_series is not None:
+            clauses.append(DistroArchSeries.id.is_in(
                 self.distro_series.enabled_architectures.get_select_expr(
                     DistroArchSeries.id)))
-        return processors.config(distinct=True)
+        else:
+            # We don't know the series until we've looked at snapcraft.yaml
+            # to dispatch a build, so enabled architectures for any active
+            # series will do.
+            clauses.extend([
+                DistroArchSeries.enabled == True,
+                DistroArchSeries.distroseriesID == DistroSeries.id,
+                DistroSeries.status.is_in(ACTIVE_STATUSES),
+                ])
+        return Store.of(self).find(Processor, *clauses).config(distinct=True)
 
     def _getProcessors(self):
         return list(Store.of(self).find(
@@ -445,16 +459,32 @@
 
     processors = property(_getProcessors, setProcessors)
 
-    def getAllowedArchitectures(self):
+    def _isBuildableArchitectureAllowed(self, das):
+        """Check whether we may build for a buildable `DistroArchSeries`.
+
+        The caller is assumed to have already checked that a suitable chroot
+        is available (either directly or via
+        `DistroSeries.buildable_architectures`).
+        """
+        return (
+            das.enabled
+            and das.processor in self.processors
+            and (
+                das.processor.supports_virtualized
+                or not self.require_virtualized))
+
+    def _isArchitectureAllowed(self, das, pocket):
+        return (
+            das.getChroot(pocket=pocket) is not None
+            and self._isBuildableArchitectureAllowed(das))
+
+    def getAllowedArchitectures(self, distro_series=None):
         """See `ISnap`."""
+        if distro_series is None:
+            distro_series = self.distro_series
         return [
-            das for das in self.distro_series.buildable_architectures
-            if (
-                das.enabled
-                and das.processor in self.processors
-                and (
-                    das.processor.supports_virtualized
-                    or not self.require_virtualized))]
+            das for das in distro_series.buildable_architectures
+            if self._isBuildableArchitectureAllowed(das)]
 
     @property
     def store_distro_series(self):
@@ -559,8 +589,8 @@
                      channels=None, build_request=None):
         """See `ISnap`."""
         self._checkRequestBuild(requester, archive)
-        if distro_arch_series not in self.getAllowedArchitectures():
-            raise SnapBuildDisallowedArchitecture(distro_arch_series)
+        if not self._isArchitectureAllowed(distro_arch_series, pocket):
+            raise SnapBuildDisallowedArchitecture(distro_arch_series, pocket)
 
         pending = IStore(self).find(
             SnapBuild,
@@ -591,6 +621,10 @@
                              allow_failures=False, fetch_snapcraft_yaml=True,
                              build_request=None, logger=None):
         """See `ISnap`."""
+        if not fetch_snapcraft_yaml and self.distro_series is None:
+            # Slightly misleading, but requestAutoBuilds is the only place
+            # this can happen right now and it raises a more specific error.
+            raise CannotRequestAutoBuilds("distro_series")
         try:
             if fetch_snapcraft_yaml:
                 try:
@@ -606,12 +640,38 @@
                     snapcraft_data = {}
             else:
                 snapcraft_data = {}
+
+            # Find a suitable base snap.
+            base_snap_set = getUtility(IBaseSnapSet)
+            if "base" in snapcraft_data:
+                base_snap_name = snapcraft_data["base"]
+                if isinstance(base_snap_name, bytes):
+                    base_snap_name = base_snap_name.decode("UTF-8")
+                base_snap = base_snap_set.getByName(base_snap_name)
+            else:
+                base_snap = base_snap_set.getDefault()
+
+            # Combine the base snap with other configuration to find a
+            # suitable distro series and suitable channels.
+            distro_series = self.distro_series
+            if base_snap is not None:
+                if distro_series is None:
+                    distro_series = base_snap.distro_series
+                new_channels = dict(base_snap.channels)
+                if channels is not None:
+                    new_channels.update(channels)
+                channels = new_channels
+            elif distro_series is None:
+                # A base snap is mandatory if there's no configured distro
+                # series.
+                raise NoSuchBaseSnap(snapcraft_data.get("base", "<default>"))
+
             # Sort by Processor.id for determinism.  This is chosen to be
             # the same order as in BinaryPackageBuildSet.createForSource, to
             # minimise confusion.
             supported_arches = OrderedDict(
                 (das.architecturetag, das) for das in sorted(
-                    self.getAllowedArchitectures(),
+                    self.getAllowedArchitectures(distro_series),
                     key=attrgetter("processor.id")))
             architectures_to_build = determine_architectures_to_build(
                 snapcraft_data, supported_arches.keys())
@@ -652,6 +712,11 @@
             raise CannotRequestAutoBuilds("auto_build_archive")
         if self.auto_build_pocket is None:
             raise CannotRequestAutoBuilds("auto_build_pocket")
+        if self.distro_series is None:
+            raise IncompatibleArguments(
+                "Cannot use requestAutoBuilds for a snap package without "
+                "distro_series being set.  Consider using requestBuilds "
+                "instead.")
         self.is_stale = False
         if logger is not None:
             logger.debug(

=== modified file 'lib/lp/snappy/templates/snap-index.pt'
--- lib/lp/snappy/templates/snap-index.pt	2018-09-27 13:51:42 +0000
+++ lib/lp/snappy/templates/snap-index.pt	2019-02-05 12:46:08 +0000
@@ -45,9 +45,11 @@
         <dt>Owner:</dt>
         <dd tal:content="structure view/person_picker"/>
       </dl>
-      <dl id="distro_series">
+      <dl id="distro_series"
+          tal:define="distro_series context/distro_series"
+          tal:condition="distro_series">
         <dt>Distribution series:</dt>
-        <dd tal:define="distro_series context/distro_series">
+        <dd>
           <a tal:attributes="href distro_series/fmt:url"
              tal:content="distro_series/fullseriesname"/>
           <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>

=== modified file 'lib/lp/snappy/templates/snap-request-builds.pt'
--- lib/lp/snappy/templates/snap-request-builds.pt	2015-09-18 13:32:09 +0000
+++ lib/lp/snappy/templates/snap-request-builds.pt	2019-02-05 12:46:08 +0000
@@ -14,9 +14,17 @@
         <tal:widget define="widget nocall:view/widgets/archive">
           <metal:block use-macro="context/@@launchpad_form/widget_row" />
         </tal:widget>
-        <tal:widget define="widget nocall:view/widgets/distro_arch_series">
-          <metal:block use-macro="context/@@launchpad_form/widget_row" />
-        </tal:widget>
+        <tal:comment condition="nothing">
+          XXX cjwatson 2019-02-05: Architecture selection is still useful
+          even when the series is guessed from snapcraft.yaml, but it's more
+          difficult to work out what choices to offer.  For now we just make
+          it all-or-nothing in that case.
+        </tal:comment>
+        <tal:has-distro-series condition="context/distro_series">
+          <tal:widget define="widget nocall:view/widgets/distro_arch_series">
+            <metal:block use-macro="context/@@launchpad_form/widget_row" />
+          </tal:widget>
+        </tal:has-distro-series>
         <tal:widget define="widget nocall:view/widgets/pocket">
           <metal:block use-macro="context/@@launchpad_form/widget_row" />
         </tal:widget>

=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py	2019-02-05 12:46:08 +0000
+++ lib/lp/snappy/tests/test_snap.py	2019-02-05 12:46:08 +0000
@@ -62,6 +62,7 @@
 from lp.registry.enums import PersonVisibility
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.registry.interfaces.series import SeriesStatus
 from lp.services.config import config
 from lp.services.database.constants import (
     ONE_DAY_AGO,
@@ -84,6 +85,10 @@
 from lp.services.webapp.interfaces import OAuthPermission
 from lp.services.webapp.publisher import canonical_url
 from lp.services.webapp.snapshot import notify_modified
+from lp.snappy.interfaces.basesnap import (
+    IBaseSnapSet,
+    NoSuchBaseSnap,
+    )
 from lp.snappy.interfaces.snap import (
     BadSnapSearchContext,
     CannotFetchSnapcraftYaml,
@@ -326,6 +331,22 @@
             SnapBuildDisallowedArchitecture, snap.requestBuild,
             snap.owner, snap.distro_series.main_archive, arches[1],
             PackagePublishingPocket.UPDATES)
+        inactive_proc = self.factory.makeProcessor(supports_virtualized=True)
+        inactive_arch = self.makeBuildableDistroArchSeries(
+            distroseries=self.factory.makeDistroSeries(
+                status=SeriesStatus.OBSOLETE),
+            processor=inactive_proc)
+        snap_no_ds = self.factory.makeSnap(distroseries=None)
+        snap_no_ds.requestBuild(
+            snap_no_ds.owner, distroseries.main_archive, arches[0],
+            PackagePublishingPocket.UPDATES)
+        snap_no_ds.requestBuild(
+            snap_no_ds.owner, distroseries.main_archive, arches[1],
+            PackagePublishingPocket.UPDATES)
+        self.assertRaises(
+            SnapBuildDisallowedArchitecture, snap.requestBuild,
+            snap.owner, snap.distro_series.main_archive, inactive_arch,
+            PackagePublishingPocket.UPDATES)
 
     def test_requestBuild_virtualization(self):
         # New builds are virtualized if any of the processor, snap or
@@ -439,6 +460,33 @@
             pocket=Equals(PackagePublishingPocket.UPDATES),
             channels=Equals({"snapcraft": "edge"})))
 
+    def test_requestBuilds_without_distroseries(self):
+        # requestBuilds schedules a job for a snap without a distroseries.
+        snap = self.factory.makeSnap(distroseries=None)
+        archive = self.factory.makeArchive()
+        now = get_transaction_timestamp(IStore(snap))
+        with person_logged_in(snap.owner.teamowner):
+            request = snap.requestBuilds(
+                snap.owner.teamowner, archive, PackagePublishingPocket.UPDATES,
+                channels={"snapcraft": "edge"})
+        self.assertThat(request, MatchesStructure(
+            date_requested=Equals(now),
+            date_finished=Is(None),
+            snap=Equals(snap),
+            status=Equals(SnapBuildRequestStatus.PENDING),
+            error_message=Is(None),
+            builds=AfterPreprocessing(set, MatchesSetwise()),
+            archive=Equals(archive)))
+        [job] = getUtility(ISnapRequestBuildsJobSource).iterReady()
+        self.assertThat(job, MatchesStructure(
+            job_id=Equals(request.id),
+            job=MatchesStructure.byEquality(status=JobStatus.WAITING),
+            snap=Equals(snap),
+            requester=Equals(snap.owner.teamowner),
+            archive=Equals(archive),
+            pocket=Equals(PackagePublishingPocket.UPDATES),
+            channels=Equals({"snapcraft": "edge"})))
+
     def makeRequestBuildsJob(self, arch_tags, git_ref=None):
         distro = self.factory.makeDistribution()
         distroseries = self.factory.makeDistroSeries(distribution=distro)
@@ -447,11 +495,9 @@
                 name=arch_tag, supports_virtualized=True)
             for arch_tag in arch_tags]
         for processor in processors:
-            das = self.factory.makeDistroArchSeries(
+            self.makeBuildableDistroArchSeries(
                 distroseries=distroseries, architecturetag=processor.name,
                 processor=processor)
-            das.addOrUpdateChroot(self.factory.makeLibraryFileAlias(
-                filename="fake_chroot.tar.gz", db_only=True))
         if git_ref is None:
             [git_ref] = self.factory.makeGitRefs()
         snap = self.factory.makeSnap(
@@ -460,15 +506,18 @@
             snap, snap.owner.teamowner, distro.main_archive,
             PackagePublishingPocket.RELEASE, {"snapcraft": "edge"})
 
-    def assertRequestedBuildsMatch(self, builds, job, arch_tags):
+    def assertRequestedBuildsMatch(self, builds, job, arch_tags, channels,
+                                   distro_series=None):
+        if distro_series is None:
+            distro_series = job.snap.distro_series
         self.assertThat(builds, MatchesSetwise(
             *(MatchesStructure(
                 requester=Equals(job.requester),
                 snap=Equals(job.snap),
                 archive=Equals(job.archive),
-                distro_arch_series=Equals(job.snap.distro_series[arch_tag]),
+                distro_arch_series=Equals(distro_series[arch_tag]),
                 pocket=Equals(job.pocket),
-                channels=Equals(job.channels))
+                channels=Equals(channels))
               for arch_tag in arch_tags)))
 
     def test_requestBuildsFromJob_restricts_explicit_list(self):
@@ -489,7 +538,7 @@
                 job.requester, job.archive, job.pocket,
                 removeSecurityProxy(job.channels),
                 build_request=job.build_request)
-        self.assertRequestedBuildsMatch(builds, job, ["sparc"])
+        self.assertRequestedBuildsMatch(builds, job, ["sparc"], job.channels)
 
     def test_requestBuildsFromJob_no_explicit_architectures(self):
         # If the snap doesn't specify any architectures,
@@ -505,7 +554,90 @@
                 job.requester, job.archive, job.pocket,
                 removeSecurityProxy(job.channels),
                 build_request=job.build_request)
-        self.assertRequestedBuildsMatch(builds, job, ["mips64el", "riscv64"])
+        self.assertRequestedBuildsMatch(
+            builds, job, ["mips64el", "riscv64"], job.channels)
+
+    def test_requestBuildsFromJob_no_distroseries_explicit_base(self):
+        # If the snap doesn't specify a distroseries but has an explicit
+        # base, requestBuildsFromJob requests builds for the appropriate
+        # distroseries for the base.
+        self.useFixture(GitHostingFixture(blob="base: test-base\n"))
+        with admin_logged_in():
+            base_snap = self.factory.makeBaseSnap(
+                name="test-base",
+                channels={"snapcraft": "stable/launchpad-buildd"})
+            self.factory.makeBaseSnap()
+        for arch_tag in ("mips64el", "riscv64"):
+            self.makeBuildableDistroArchSeries(
+                distroseries=base_snap.distro_series, architecturetag=arch_tag,
+                processor=self.factory.makeProcessor(
+                    name=arch_tag, supports_virtualized=True))
+        snap = self.factory.makeSnap(
+            distroseries=None, git_ref=self.factory.makeGitRefs()[0])
+        job = getUtility(ISnapRequestBuildsJobSource).create(
+            snap, snap.owner.teamowner, base_snap.distro_series.main_archive,
+            PackagePublishingPocket.RELEASE, None)
+        self.assertEqual(
+            get_transaction_timestamp(IStore(snap)), job.date_created)
+        transaction.commit()
+        with person_logged_in(job.requester):
+            builds = snap.requestBuildsFromJob(
+                job.requester, job.archive, job.pocket,
+                build_request=job.build_request)
+        self.assertRequestedBuildsMatch(
+            builds, job, ["mips64el", "riscv64"], base_snap.channels,
+            distro_series=base_snap.distro_series)
+
+    def test_requestBuildsFromJob_no_distroseries_no_explicit_base(self):
+        # If the snap doesn't specify a distroseries and has no explicit
+        # base, requestBuildsFromJob requests builds for the appropriate
+        # distroseries for the default base snap.
+        self.useFixture(GitHostingFixture(blob="name: foo\n"))
+        with admin_logged_in():
+            base_snap = self.factory.makeBaseSnap(
+                channels={"snapcraft": "stable/launchpad-buildd"})
+            getUtility(IBaseSnapSet).setDefault(base_snap)
+            self.factory.makeBaseSnap()
+        for arch_tag in ("mips64el", "riscv64"):
+            self.makeBuildableDistroArchSeries(
+                distroseries=base_snap.distro_series, architecturetag=arch_tag,
+                processor=self.factory.makeProcessor(
+                    name=arch_tag, supports_virtualized=True))
+        snap = self.factory.makeSnap(
+            distroseries=None, git_ref=self.factory.makeGitRefs()[0])
+        job = getUtility(ISnapRequestBuildsJobSource).create(
+            snap, snap.owner.teamowner, base_snap.distro_series.main_archive,
+            PackagePublishingPocket.RELEASE, None)
+        self.assertEqual(
+            get_transaction_timestamp(IStore(snap)), job.date_created)
+        transaction.commit()
+        with person_logged_in(job.requester):
+            builds = snap.requestBuildsFromJob(
+                job.requester, job.archive, job.pocket,
+                build_request=job.build_request)
+        self.assertRequestedBuildsMatch(
+            builds, job, ["mips64el", "riscv64"], base_snap.channels,
+            distro_series=base_snap.distro_series)
+
+    def test_requestBuildsFromJob_no_distroseries_no_default_base(self):
+        # If the snap doesn't specify a distroseries and has an explicit
+        # base, and there is no default base snap, requestBuildsFromJob
+        # gives up.
+        self.useFixture(GitHostingFixture(blob="name: foo\n"))
+        with admin_logged_in():
+            base_snap = self.factory.makeBaseSnap(
+                channels={"snapcraft": "stable/launchpad-buildd"})
+        snap = self.factory.makeSnap(
+            distroseries=None, git_ref=self.factory.makeGitRefs()[0])
+        job = getUtility(ISnapRequestBuildsJobSource).create(
+            snap, snap.owner.teamowner, base_snap.distro_series.main_archive,
+            PackagePublishingPocket.RELEASE, None)
+        transaction.commit()
+        with person_logged_in(job.requester):
+            self.assertRaises(
+                NoSuchBaseSnap, snap.requestBuildsFromJob,
+                job.requester, job.archive, job.pocket,
+                build_request=job.build_request)
 
     def test_requestBuildsFromJob_unsupported_remote(self):
         # If the snap is based on an external Git repository from which we
@@ -523,7 +655,8 @@
                 job.requester, job.archive, job.pocket,
                 removeSecurityProxy(job.channels),
                 build_request=job.build_request)
-        self.assertRequestedBuildsMatch(builds, job, ["mips64el", "riscv64"])
+        self.assertRequestedBuildsMatch(
+            builds, job, ["mips64el", "riscv64"], job.channels)
 
     def test_requestBuildsFromJob_triggers_webhooks(self):
         # requestBuildsFromJob triggers webhooks, and the payload includes a
@@ -1911,6 +2044,29 @@
         self.arm = self.factory.makeProcessor(
             name="arm", restricted=True, build_by_default=False)
 
+    def test_available_processors_with_distro_series(self):
+        # If the snap has a distroseries, only those processors that are
+        # enabled for that series are available.
+        distroseries = self.factory.makeDistroSeries()
+        for processor in self.default_procs:
+            self.factory.makeDistroArchSeries(
+                distroseries=distroseries, architecturetag=processor.name,
+                processor=processor)
+        self.factory.makeDistroArchSeries(
+            architecturetag=self.arm.name, processor=self.arm)
+        snap = self.factory.makeSnap(distroseries=distroseries)
+        self.assertContentEqual(self.default_procs, snap.available_processors)
+
+    def test_available_processors_without_distro_series(self):
+        # If the snap does not have a distroseries, then processors that are
+        # enabled for any active series are available.
+        snap = self.factory.makeSnap(distroseries=None)
+        # 386 and hppa have corresponding DASes in sampledata for active
+        # distroseries.
+        self.assertContentEqual(
+            ["386", "hppa"],
+            [processor.name for processor in snap.available_processors])
+
     def test_new_default_processors(self):
         # SnapSet.new creates a SnapArch for each available Processor with
         # build_by_default set.

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2019-02-05 12:46:08 +0000
+++ lib/lp/testing/factory.py	2019-02-05 12:46:08 +0000
@@ -4711,7 +4711,7 @@
             target, self.makePerson(), delivery_url, event_types or [],
             active, secret)
 
-    def makeSnap(self, registrant=None, owner=None, distroseries=None,
+    def makeSnap(self, registrant=None, owner=None, distroseries=_DEFAULT,
                  name=None, branch=None, git_ref=None, auto_build=False,
                  auto_build_archive=None, auto_build_pocket=None,
                  auto_build_channels=None, is_stale=None,
@@ -4725,7 +4725,7 @@
             registrant = self.makePerson()
         if owner is None:
             owner = self.makeTeam(registrant)
-        if distroseries is None:
+        if distroseries is _DEFAULT:
             distroseries = self.makeDistroSeries()
         if name is None:
             name = self.getUniqueString(u"snap-name")
@@ -4785,7 +4785,7 @@
                 distroseries = self.makeDistroSeries(
                     distribution=archive.distribution)
             else:
-                distroseries = None
+                distroseries = _DEFAULT
             if registrant is None:
                 registrant = requester
             snap = self.makeSnap(


Follow ups