← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:snap-base-arch into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:snap-base-arch into launchpad:master.

Commit message:
Add and use SnapBaseArch

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1862258 in Launchpad itself: "base: core20 snap builds are dispatched for i386, which always fails"
  https://bugs.launchpad.net/launchpad/+bug/1862258

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/402533

A snap base may not support all the architectures supported by its underlying distroseries: in particular, core20 does not support i386.  Add a `SnapBaseArch` table along the same lines as the existing `SnapArch` (but with a simpler `setProcessors`, since only registry experts and admins can edit `SnapBase` anyway), and intersect the architectures supported by the snap base with other existing constraints when dispatching snap builds.

DB MP: https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/402532
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:snap-base-arch into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index c3aba65..b13d4a8 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -301,6 +301,7 @@ public.sharingjob                       = SELECT, INSERT, UPDATE, DELETE
 public.snap                             = SELECT, INSERT, UPDATE, DELETE
 public.snaparch                         = SELECT, INSERT, DELETE
 public.snapbase                         = SELECT, INSERT, UPDATE, DELETE
+public.snapbasearch                     = SELECT, INSERT, DELETE
 public.snapbuild                        = SELECT, INSERT, UPDATE, DELETE
 public.snapbuildjob                     = SELECT, INSERT, UPDATE, DELETE
 public.snapfile                         = SELECT, INSERT, UPDATE, DELETE
@@ -842,6 +843,7 @@ public.product                                  = SELECT
 public.snap                                     = SELECT, UPDATE
 public.snaparch                                 = SELECT
 public.snapbase                                 = SELECT
+public.snapbasearch                             = SELECT
 public.snapbuild                                = SELECT, INSERT
 public.snapbuildjob                             = SELECT
 public.sourcepackagename                        = SELECT
@@ -1032,6 +1034,7 @@ public.seriessourcepackagebranch              = SELECT
 public.snap                                   = SELECT
 public.snaparch                               = SELECT
 public.snapbase                               = SELECT
+public.snapbasearch                           = SELECT
 public.snapbuild                              = SELECT, UPDATE
 public.snapbuildjob                           = SELECT, INSERT
 public.snapfile                               = SELECT
@@ -1490,6 +1493,7 @@ public.signedcodeofconduct              = SELECT
 public.snap                             = SELECT, UPDATE
 public.snaparch                         = SELECT
 public.snapbase                         = SELECT
+public.snapbasearch                     = SELECT
 public.snapbuild                        = SELECT, UPDATE
 public.snapbuildjob                     = SELECT, INSERT, UPDATE
 public.snapfile                         = SELECT, INSERT, UPDATE
@@ -2719,6 +2723,7 @@ public.product                          = SELECT
 public.snap                             = SELECT, UPDATE
 public.snaparch                         = SELECT
 public.snapbase                         = SELECT
+public.snapbasearch                     = SELECT
 public.snapbuild                        = SELECT, INSERT, UPDATE
 public.snapbuildjob                     = SELECT, UPDATE
 public.snapfile                         = SELECT
diff --git a/lib/lp/snappy/interfaces/snapbase.py b/lib/lp/snappy/interfaces/snapbase.py
index a5fdf90..21e4729 100644
--- a/lib/lp/snappy/interfaces/snapbase.py
+++ b/lib/lp/snappy/interfaces/snapbase.py
@@ -44,12 +44,14 @@ from zope.schema import (
     Datetime,
     Dict,
     Int,
+    List,
     TextLine,
     )
 
 from lp import _
 from lp.app.errors import NameLookupFailed
 from lp.app.validators.name import name_validator
+from lp.buildmaster.interfaces.processor import IProcessor
 from lp.registry.interfaces.distroseries import IDistroSeries
 from lp.services.fields import (
     ContentNameField,
@@ -125,6 +127,12 @@ class ISnapBaseView(Interface):
             could not be found.
         """
 
+    processors = exported(CollectionField(
+        title=_("Processors"),
+        description=_("The architectures that the snap base supports."),
+        value_type=Reference(schema=IProcessor),
+        readonly=True))
+
 
 class ISnapBaseEditableAttributes(Interface):
     """`ISnapBase` attributes that can be edited.
@@ -197,6 +205,14 @@ class ISnapBaseEdit(Interface):
         :param dependency: an `IArchive`.
         """
 
+    @operation_parameters(
+        processors=List(
+            value_type=Reference(schema=IProcessor), required=True))
+    @export_write_operation()
+    @operation_for_version("devel")
+    def setProcessors(processors):
+        """Set the architectures that the snap base supports."""
+
     @export_destructor_operation()
     @operation_for_version("devel")
     def destroySelf():
@@ -218,11 +234,14 @@ class ISnapBaseSetEdit(Interface):
     """`ISnapBaseSet` methods that require launchpad.Edit permission."""
 
     @call_with(registrant=REQUEST_USER)
+    @operation_parameters(
+        processors=List(
+            value_type=Reference(schema=IProcessor), required=False))
     @export_factory_operation(
         ISnapBase, ["name", "display_name", "distro_series", "build_channels"])
     @operation_for_version("devel")
     def new(registrant, name, display_name, distro_series, build_channels,
-            date_created=None):
+            processors=None, date_created=None):
         """Create an `ISnapBase`."""
 
     @operation_parameters(
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index 1a108b0..4dc26e5 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -612,7 +612,7 @@ class Snap(Storm, WebhookTargetMixin):
 
     processors = property(_getProcessors, setProcessors)
 
-    def _isBuildableArchitectureAllowed(self, das):
+    def _isBuildableArchitectureAllowed(self, das, snap_base=None):
         """Check whether we may build for a buildable `DistroArchSeries`.
 
         The caller is assumed to have already checked that a suitable chroot
@@ -624,20 +624,21 @@ class Snap(Storm, WebhookTargetMixin):
             and das.processor in self.processors
             and (
                 das.processor.supports_virtualized
-                or not self.require_virtualized))
+                or not self.require_virtualized)
+            and (snap_base is None or das.processor in snap_base.processors))
 
-    def _isArchitectureAllowed(self, das, pocket):
+    def _isArchitectureAllowed(self, das, pocket, snap_base=None):
         return (
             das.getChroot(pocket=pocket) is not None
-            and self._isBuildableArchitectureAllowed(das))
+            and self._isBuildableArchitectureAllowed(das, snap_base=snap_base))
 
-    def getAllowedArchitectures(self, distro_series=None):
+    def getAllowedArchitectures(self, distro_series=None, snap_base=None):
         """See `ISnap`."""
         if distro_series is None:
             distro_series = self.distro_series
         return [
             das for das in distro_series.buildable_architectures
-            if self._isBuildableArchitectureAllowed(das)]
+            if self._isBuildableArchitectureAllowed(das, snap_base=snap_base)]
 
     @property
     def store_distro_series(self):
@@ -779,7 +780,8 @@ class Snap(Storm, WebhookTargetMixin):
                      snap_base=None, channels=None, build_request=None):
         """See `ISnap`."""
         self._checkRequestBuild(requester, archive)
-        if not self._isArchitectureAllowed(distro_arch_series, pocket):
+        if not self._isArchitectureAllowed(
+                distro_arch_series, pocket, snap_base=snap_base):
             raise SnapBuildDisallowedArchitecture(distro_arch_series, pocket)
 
         if not channels:
@@ -898,7 +900,8 @@ class Snap(Storm, WebhookTargetMixin):
             # minimise confusion.
             supported_arches = OrderedDict(
                 (das.architecturetag, das) for das in sorted(
-                    self.getAllowedArchitectures(distro_series),
+                    self.getAllowedArchitectures(
+                        distro_series, snap_base=snap_base),
                     key=attrgetter("processor.id"))
                 if (architectures is None or
                     das.architecturetag in architectures))
diff --git a/lib/lp/snappy/model/snapbase.py b/lib/lp/snappy/model/snapbase.py
index 8916c61..c2f2a76 100644
--- a/lib/lp/snappy/model/snapbase.py
+++ b/lib/lp/snappy/model/snapbase.py
@@ -27,6 +27,7 @@ from zope.interface import implementer
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.errors import NotFoundError
+from lp.buildmaster.model.processor import Processor
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.model.person import Person
 from lp.services.database.constants import DEFAULT
@@ -85,6 +86,30 @@ class SnapBase(Storm):
         self.date_created = date_created
         self.is_default = False
 
+    def _getProcessors(self):
+        return list(Store.of(self).find(
+            Processor,
+            Processor.id == SnapBaseArch.processor_id,
+            SnapBaseArch.snap_base == self))
+
+    def setProcessors(self, processors):
+        """See `ISnapBase`."""
+        enablements = dict(Store.of(self).find(
+            (Processor, SnapBaseArch),
+            Processor.id == SnapBaseArch.processor_id,
+            SnapBaseArch.snap_base == self))
+        for proc in enablements:
+            if proc not in processors:
+                Store.of(self).remove(enablements[proc])
+        for proc in processors:
+            if proc not in self.processors:
+                snap_base_arch = SnapBaseArch()
+                snap_base_arch.snap_base = self
+                snap_base_arch.processor = proc
+                Store.of(self).add(snap_base_arch)
+
+    processors = property(_getProcessors, setProcessors)
+
     @property
     def dependencies(self):
         """See `ISnapBase`."""
@@ -145,18 +170,35 @@ class SnapBase(Storm):
         Store.of(self).remove(self)
 
 
+class SnapBaseArch(Storm):
+    """Link table to back `SnapArch.processors`."""
+
+    __storm_table__ = "SnapBaseArch"
+    __storm_primary__ = ("snap_base_id", "processor_id")
+
+    snap_base_id = Int(name="snap_base", allow_none=False)
+    snap_base = Reference(snap_base_id, "SnapBase.id")
+
+    processor_id = Int(name="processor", allow_none=False)
+    processor = Reference(processor_id, "Processor.id")
+
+
 @implementer(ISnapBaseSet)
 class SnapBaseSet:
     """See `ISnapBaseSet`."""
 
     def new(self, registrant, name, display_name, distro_series,
-            build_channels, date_created=DEFAULT):
+            build_channels, processors=None, date_created=DEFAULT):
         """See `ISnapBaseSet`."""
         store = IMasterStore(SnapBase)
         snap_base = SnapBase(
             registrant, name, display_name, distro_series, build_channels,
             date_created=date_created)
         store.add(snap_base)
+        if processors is None:
+            processors = [
+                das.processor for das in distro_series.enabled_architectures]
+        snap_base.setProcessors(processors)
         return snap_base
 
     def __iter__(self):
diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
index be338d7..50d1269 100644
--- a/lib/lp/snappy/tests/test_snap.py
+++ b/lib/lp/snappy/tests/test_snap.py
@@ -300,7 +300,8 @@ class TestSnap(TestCaseWithFactory):
         snap = self.factory.makeSnap(
             distroseries=distroarchseries.distroseries, processors=[processor])
         with admin_logged_in():
-            snap_base = self.factory.makeSnapBase()
+            snap_base = self.factory.makeSnapBase(
+                distro_series=distroarchseries.distroseries)
         build = snap.requestBuild(
             snap.owner, snap.distro_series.main_archive, distroarchseries,
             PackagePublishingPocket.UPDATES, snap_base=snap_base)
@@ -745,20 +746,21 @@ class TestSnap(TestCaseWithFactory):
         # base, requestBuildsFromJob requests builds for the appropriate
         # distroseries for the base.
         self.useFixture(GitHostingFixture(blob="base: test-base\n"))
-        with admin_logged_in():
-            snap_base = self.factory.makeSnapBase(
-                name="test-base",
-                build_channels={"snapcraft": "stable/launchpad-buildd"})
-            self.factory.makeSnapBase()
+        distroseries = self.factory.makeDistroSeries()
         for arch_tag in ("mips64el", "riscv64"):
             self.makeBuildableDistroArchSeries(
-                distroseries=snap_base.distro_series, architecturetag=arch_tag,
+                distroseries=distroseries, architecturetag=arch_tag,
                 processor=self.factory.makeProcessor(
                     name=arch_tag, supports_virtualized=True))
+        with admin_logged_in():
+            snap_base = self.factory.makeSnapBase(
+                name="test-base", distro_series=distroseries,
+                build_channels={"snapcraft": "stable/launchpad-buildd"})
+            self.factory.makeSnapBase()
         snap = self.factory.makeSnap(
             distroseries=None, git_ref=self.factory.makeGitRefs()[0])
         job = getUtility(ISnapRequestBuildsJobSource).create(
-            snap, snap.owner.teamowner, snap_base.distro_series.main_archive,
+            snap, snap.owner.teamowner, distroseries.main_archive,
             PackagePublishingPocket.RELEASE, None)
         self.assertEqual(
             get_transaction_timestamp(IStore(snap)), job.date_created)
@@ -769,27 +771,29 @@ class TestSnap(TestCaseWithFactory):
                 build_request=job.build_request)
         self.assertRequestedBuildsMatch(
             builds, job, ["mips64el", "riscv64"], snap_base,
-            snap_base.build_channels, distro_series=snap_base.distro_series)
+            snap_base.build_channels, distro_series=distroseries)
 
     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.
         self.useFixture(GitHostingFixture(blob="name: foo\n"))
+        distroseries = self.factory.makeDistroSeries()
+        for arch_tag in ("mips64el", "riscv64"):
+            self.makeBuildableDistroArchSeries(
+                distroseries=distroseries, architecturetag=arch_tag,
+                processor=self.factory.makeProcessor(
+                    name=arch_tag, supports_virtualized=True))
         with admin_logged_in():
             snap_base = self.factory.makeSnapBase(
+                distro_series=distroseries,
                 build_channels={"snapcraft": "stable/launchpad-buildd"})
             getUtility(ISnapBaseSet).setDefault(snap_base)
             self.factory.makeSnapBase()
-        for arch_tag in ("mips64el", "riscv64"):
-            self.makeBuildableDistroArchSeries(
-                distroseries=snap_base.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, snap_base.distro_series.main_archive,
+            snap, snap.owner.teamowner, distroseries.main_archive,
             PackagePublishingPocket.RELEASE, None)
         self.assertEqual(
             get_transaction_timestamp(IStore(snap)), job.date_created)
@@ -800,7 +804,7 @@ class TestSnap(TestCaseWithFactory):
                 build_request=job.build_request)
         self.assertRequestedBuildsMatch(
             builds, job, ["mips64el", "riscv64"], snap_base,
-            snap_base.build_channels, distro_series=snap_base.distro_series)
+            snap_base.build_channels, distro_series=distroseries)
 
     def test_requestBuildsFromJob_no_distroseries_no_default_base(self):
         # If the snap doesn't specify a distroseries and has an explicit
@@ -821,6 +825,40 @@ class TestSnap(TestCaseWithFactory):
                 job.requester, job.archive, job.pocket,
                 build_request=job.build_request)
 
+    def test_requestBuildsFromJob_snap_base_architectures(self):
+        # requestBuildsFromJob intersects the architectures supported by the
+        # snap base with any other constraints.
+        self.useFixture(GitHostingFixture(blob="base: test-base\n"))
+        processors = [
+            self.factory.makeProcessor(supports_virtualized=True)
+            for _ in range(3)]
+        distroseries = self.factory.makeDistroSeries()
+        for processor in processors:
+            self.makeBuildableDistroArchSeries(
+                distroseries=distroseries, architecturetag=processor.name,
+                processor=processor)
+        with admin_logged_in():
+            snap_base = self.factory.makeSnapBase(
+                name="test-base", distro_series=distroseries,
+                build_channels={"snapcraft": "stable/launchpad-buildd"},
+                processors=processors[:2])
+        snap = self.factory.makeSnap(
+            distroseries=None, git_ref=self.factory.makeGitRefs()[0])
+        job = getUtility(ISnapRequestBuildsJobSource).create(
+            snap, snap.owner.teamowner, snap_base.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, [processor.name for processor in processors[:2]],
+            snap_base, snap_base.build_channels,
+            distro_series=snap_base.distro_series)
+
     def test_requestBuildsFromJob_unsupported_remote(self):
         # If the snap is based on an external Git repository from which we
         # don't support fetching blobs, requestBuildsFromJob falls back to
@@ -2544,12 +2582,14 @@ class TestSnapSet(TestCaseWithFactory):
     def test_makeAutoBuilds_infers_distroseries(self):
         # ISnapSet.makeAutoBuilds can infer the series of a snap from the base
         # specified in its snapcraft.yaml.
-        with admin_logged_in():
-            snap_base = self.factory.makeSnapBase(name="core20")
+        distroseries = self.factory.makeDistroSeries()
         das = self.makeBuildableDistroArchSeries(
-            distroseries=snap_base.distro_series, architecturetag='riscv64',
+            distroseries=distroseries, architecturetag='riscv64',
             processor=self.factory.makeProcessor(
                 name='riscv64', supports_virtualized=True))
+        with admin_logged_in():
+            snap_base = self.factory.makeSnapBase(
+                name="core20", distro_series=distroseries)
         [git_ref] = self.factory.makeGitRefs()
         owner = self.factory.makePerson()
         snap = self.factory.makeSnap(
diff --git a/lib/lp/snappy/tests/test_snapbase.py b/lib/lp/snappy/tests/test_snapbase.py
index 67754d2..c528f40 100644
--- a/lib/lp/snappy/tests/test_snapbase.py
+++ b/lib/lp/snappy/tests/test_snapbase.py
@@ -31,6 +31,7 @@ from lp.snappy.interfaces.snapbase import (
     )
 from lp.soyuz.interfaces.component import IComponentSet
 from lp.testing import (
+    admin_logged_in,
     api_url,
     celebrity_logged_in,
     logout,
@@ -78,6 +79,56 @@ class TestSnapBase(TestCaseWithFactory):
         self.assertRaises(CannotDeleteSnapBase, snap_base.destroySelf)
 
 
+class TestSnapBaseProcessors(TestCaseWithFactory):
+
+    layer = ZopelessDatabaseLayer
+
+    def setUp(self):
+        super(TestSnapBaseProcessors, self).setUp(user="foo.bar@xxxxxxxxxxxxx")
+        self.unrestricted_procs = [
+            self.factory.makeProcessor() for _ in range(3)]
+        self.restricted_procs = [
+            self.factory.makeProcessor(restricted=True, build_by_default=False)
+            for _ in range(2)]
+        self.procs = self.unrestricted_procs + self.restricted_procs
+        self.factory.makeProcessor()
+        self.distroseries = self.factory.makeDistroSeries()
+        for processor in self.procs:
+            self.factory.makeDistroArchSeries(
+                distroseries=self.distroseries, architecturetag=processor.name,
+                processor=processor)
+
+    def test_new_default_processors(self):
+        # SnapBaseSet.new creates a SnapBaseArch for each available
+        # Processor for the corresponding series.
+        snap_base = getUtility(ISnapBaseSet).new(
+            registrant=self.factory.makePerson(),
+            name=self.factory.getUniqueUnicode(),
+            display_name=self.factory.getUniqueUnicode(),
+            distro_series=self.distroseries, build_channels={})
+        self.assertContentEqual(self.procs, snap_base.processors)
+
+    def test_new_override_processors(self):
+        # SnapBaseSet.new can be given a custom set of processors.
+        snap_base = getUtility(ISnapBaseSet).new(
+            registrant=self.factory.makePerson(),
+            name=self.factory.getUniqueUnicode(),
+            display_name=self.factory.getUniqueUnicode(),
+            distro_series=self.distroseries, build_channels={},
+            processors=self.procs[:2])
+        self.assertContentEqual(self.procs[:2], snap_base.processors)
+
+    def test_set(self):
+        # The property remembers its value correctly.
+        snap_base = self.factory.makeSnapBase()
+        snap_base.setProcessors(self.restricted_procs)
+        self.assertContentEqual(self.restricted_procs, snap_base.processors)
+        snap_base.setProcessors(self.procs)
+        self.assertContentEqual(self.procs, snap_base.processors)
+        snap_base.setProcessors([])
+        self.assertContentEqual([], snap_base.processors)
+
+
 class TestSnapBaseSet(TestCaseWithFactory):
 
     layer = ZopelessDatabaseLayer
@@ -368,6 +419,71 @@ class TestSnapBaseWebservice(TestCaseWithFactory):
         with person_logged_in(person):
             self.assertEqual([], list(snap_base.dependencies))
 
+    def setUpProcessors(self):
+        self.unrestricted_procs = [
+            self.factory.makeProcessor() for _ in range(3)]
+        self.unrestricted_proc_names = [
+            processor.name for processor in self.unrestricted_procs]
+        self.restricted_procs = [
+            self.factory.makeProcessor(restricted=True, build_by_default=False)
+            for _ in range(2)]
+        self.restricted_proc_names = [
+            processor.name for processor in self.restricted_procs]
+        self.procs = self.unrestricted_procs + self.restricted_procs
+        self.factory.makeProcessor()
+        self.distroseries = self.factory.makeDistroSeries()
+        for processor in self.procs:
+            self.factory.makeDistroArchSeries(
+                distroseries=self.distroseries, architecturetag=processor.name,
+                processor=processor)
+
+    def setProcessors(self, user, snap_base_url, names):
+        ws = webservice_for_person(
+            user, permission=OAuthPermission.WRITE_PUBLIC)
+        return ws.named_post(
+            snap_base_url, "setProcessors",
+            processors=["/+processors/%s" % name for name in names],
+            api_version="devel")
+
+    def assertProcessors(self, user, snap_base_url, names):
+        body = webservice_for_person(user).get(
+            snap_base_url + "/processors", api_version="devel").jsonBody()
+        self.assertContentEqual(
+            names, [entry["name"] for entry in body["entries"]])
+
+    def test_setProcessors_admin(self):
+        """An admin can change the supported processor set."""
+        self.setUpProcessors()
+        with admin_logged_in():
+            snap_base = self.factory.makeSnapBase(
+                distro_series=self.distroseries,
+                processors=self.unrestricted_procs)
+            snap_base_url = api_url(snap_base)
+        admin = self.factory.makeAdministrator()
+        self.assertProcessors(
+            admin, snap_base_url, self.unrestricted_proc_names)
+
+        response = self.setProcessors(
+            admin, snap_base_url,
+            [self.unrestricted_proc_names[0], self.restricted_proc_names[0]])
+        self.assertEqual(200, response.status)
+        self.assertProcessors(
+            admin, snap_base_url,
+            [self.unrestricted_proc_names[0], self.restricted_proc_names[0]])
+
+    def test_setProcessors_non_admin_forbidden(self):
+        """Only admins and registry experts can call setProcessors."""
+        self.setUpProcessors()
+        with admin_logged_in():
+            snap_base = self.factory.makeSnapBase(
+                distro_series=self.distroseries)
+            snap_base_url = api_url(snap_base)
+        person = self.factory.makePerson()
+
+        response = self.setProcessors(
+            person, snap_base_url, [self.unrestricted_proc_names[0]])
+        self.assertEqual(401, response.status)
+
     def test_collection(self):
         # lp.snap_bases is a collection of all SnapBases.
         person = self.factory.makePerson()
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index e95e83d..e65a596 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -4911,7 +4911,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
         return snappy_series
 
     def makeSnapBase(self, registrant=None, name=None, display_name=None,
-                     distro_series=None, build_channels=None,
+                     distro_series=None, build_channels=None, processors=None,
                      date_created=DEFAULT):
         """Make a new SnapBase."""
         if registrant is None:
@@ -4927,7 +4927,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
             build_channels = {u"snapcraft": u"stable"}
         return getUtility(ISnapBaseSet).new(
             registrant, name, display_name, distro_series, build_channels,
-            date_created=date_created)
+            processors=processors, date_created=date_created)
 
     def makeOCIProjectName(self, name=None):
         if name is None: