← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/snap-export-request-auto-builds into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-export-request-auto-builds into lp:launchpad.

Commit message:
Break out the per-snap part of SnapSet.makeAutoBuilds into Snap.requestAutoBuilds, and export it.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-export-request-auto-builds/+merge/300367

Break out the per-snap part of SnapSet.makeAutoBuilds into Snap.requestAutoBuilds, and export it.

William requested this last week, and it seems reasonable; one might wish to trigger a build with the usual parameters for an automatic build even when it happens that Launchpad can't detect that the package is stale.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-export-request-auto-builds into lp:launchpad.
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py	2016-07-15 17:11:09 +0000
+++ lib/lp/snappy/interfaces/snap.py	2016-07-18 17:19:30 +0000
@@ -8,8 +8,10 @@
 __all__ = [
     'BadSnapSearchContext',
     'CannotModifySnapProcessor',
+    'CannotRequestAutoBuilds',
     'DuplicateSnapName',
     'ISnap',
+    'ISnapEdit',
     'ISnapSet',
     'ISnapView',
     'NoSourceForSnap',
@@ -41,6 +43,7 @@
     exported,
     operation_for_version,
     operation_parameters,
+    operation_returns_collection_of,
     operation_returns_entry,
     REQUEST_USER,
     )
@@ -68,8 +71,8 @@
     )
 
 from lp import _
+from lp.app.errors import NameLookupFailed
 from lp.app.interfaces.launchpad import IPrivacy
-from lp.app.errors import NameLookupFailed
 from lp.app.validators.name import name_validator
 from lp.buildmaster.interfaces.processor import IProcessor
 from lp.code.interfaces.branch import IBranch
@@ -203,6 +206,16 @@
             self._fmt % {'processor': processor.name})
 
 
+@error_status(httplib.BAD_REQUEST)
+class CannotRequestAutoBuilds(Exception):
+    """Snap package is not configured for automatic builds."""
+
+    def __init__(self, field):
+        super(CannotRequestAutoBuilds, self).__init__(
+            "This snap package cannot have automatic builds created for it "
+            "because %s is not set." % field)
+
+
 class ISnapView(Interface):
     """`ISnap` attributes that require launchpad.View permission."""
 
@@ -247,7 +260,7 @@
         archive=Reference(schema=IArchive),
         distro_arch_series=Reference(schema=IDistroArchSeries),
         pocket=Choice(vocabulary=PackagePublishingPocket))
-    # Really ISnapBuild, patched in _schema_circular_imports.py.
+    # Really ISnapBuild, patched in lp.snappy.interfaces.webservice.
     @export_factory_operation(Interface, [])
     @operation_for_version("devel")
     def requestBuild(requester, archive, distro_arch_series, pocket):
@@ -280,7 +293,7 @@
         description=_(
             "All builds of this snap package, sorted in descending order "
             "of finishing (or starting if not completed successfully)."),
-        # Really ISnapBuild, patched in _schema_circular_imports.py.
+        # Really ISnapBuild, patched in lp.snappy.interfaces.webservice.
         value_type=Reference(schema=Interface), readonly=True)))
 
     completed_builds = exported(doNotSnapshot(CollectionField(
@@ -288,7 +301,7 @@
         description=_(
             "Completed builds of this snap package, sorted in descending "
             "order of finishing."),
-        # Really ISnapBuild, patched in _schema_circular_imports.py.
+        # Really ISnapBuild, patched in lp.snappy.interfaces.webservice.
         value_type=Reference(schema=Interface), readonly=True)))
 
     pending_builds = exported(doNotSnapshot(CollectionField(
@@ -296,13 +309,26 @@
         description=_(
             "Pending builds of this snap package, sorted in descending "
             "order of creation."),
-        # Really ISnapBuild, patched in _schema_circular_imports.py.
+        # Really ISnapBuild, patched in lp.snappy.interfaces.webservice.
         value_type=Reference(schema=Interface), readonly=True)))
 
 
 class ISnapEdit(IWebhookTarget):
     """`ISnap` methods that require launchpad.Edit permission."""
 
+    # Really ISnapBuild, patched in lp.snappy.interfaces.webservice.
+    @operation_returns_collection_of(Interface)
+    @export_write_operation()
+    @operation_for_version("devel")
+    def requestAutoBuilds(logger=None):
+        """Create and return automatic builds for this snap package.
+
+        :param logger: An optional logger.
+        :raises CannotRequestAutoBuilds: if no auto_build_archive or
+            auto_build_pocket is set.
+        :return: A sequence of `ISnapBuild` instances.
+        """
+
     @export_destructor_operation()
     @operation_for_version("devel")
     def destroySelf():
@@ -468,16 +494,21 @@
     export_as_webservice_collection(ISnap)
 
     @call_with(registrant=REQUEST_USER)
+    @operation_parameters(
+        processors=List(
+            value_type=Reference(schema=IProcessor), required=False))
     @export_factory_operation(
         ISnap, [
             "owner", "distro_series", "name", "description", "branch",
-            "git_ref", "private"])
+            "git_ref", "auto_build", "auto_build_archive", "auto_build_pocket",
+            "private"])
     @operation_for_version("devel")
     def new(registrant, owner, distro_series, name, description=None,
-            branch=None, git_ref=None, require_virtualized=True,
-            processors=None, date_created=None, private=False,
-            store_upload=False, store_series=None, store_name=None,
-            store_secrets=None):
+            branch=None, git_ref=None, auto_build=False,
+            auto_build_archive=None, auto_build_pocket=None,
+            require_virtualized=True, processors=None, date_created=None,
+            private=False, store_upload=False, store_series=None,
+            store_name=None, store_secrets=None):
         """Create an `ISnap`."""
 
     def exists(owner, name):

=== modified file 'lib/lp/snappy/interfaces/webservice.py'
--- lib/lp/snappy/interfaces/webservice.py	2016-05-05 20:02:01 +0000
+++ lib/lp/snappy/interfaces/webservice.py	2016-07-18 17:19:30 +0000
@@ -19,11 +19,13 @@
 
 from lp.services.webservice.apihelpers import (
     patch_collection_property,
+    patch_collection_return_type,
     patch_entry_return_type,
     patch_reference_property,
     )
 from lp.snappy.interfaces.snap import (
     ISnap,
+    ISnapEdit,
     ISnapSet,
     ISnapView,
     )
@@ -45,3 +47,6 @@
 patch_collection_property(ISnapView, 'builds', ISnapBuild)
 patch_collection_property(ISnapView, 'completed_builds', ISnapBuild)
 patch_collection_property(ISnapView, 'pending_builds', ISnapBuild)
+
+# ISnapEdit
+patch_collection_return_type(ISnapEdit, 'requestAutoBuilds', ISnapBuild)

=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py	2016-07-15 17:11:09 +0000
+++ lib/lp/snappy/model/snap.py	2016-07-18 17:19:30 +0000
@@ -10,6 +10,7 @@
     datetime,
     timedelta,
     )
+
 import pytz
 from storm.expr import (
     And,
@@ -96,6 +97,7 @@
 from lp.snappy.interfaces.snap import (
     BadSnapSearchContext,
     CannotModifySnapProcessor,
+    CannotRequestAutoBuilds,
     DuplicateSnapName,
     ISnap,
     ISnapSet,
@@ -380,6 +382,40 @@
         result.order_by(order_by)
         return result
 
+    def requestAutoBuilds(self, logger=None):
+        """See `ISnapSet`."""
+        builds = []
+        if self.auto_build_archive is None:
+            raise CannotRequestAutoBuilds("auto_build_archive")
+        if self.auto_build_pocket is None:
+            raise CannotRequestAutoBuilds("auto_build_pocket")
+        self.is_stale = False
+        if logger is not None:
+            logger.debug(
+                "Scheduling builds of snap package %s/%s",
+                self.owner.name, self.name)
+        for arch in self.getAllowedArchitectures():
+            try:
+                build = self.requestBuild(
+                    self.owner, self.auto_build_archive, arch,
+                    self.auto_build_pocket)
+                if logger is not None:
+                    logger.debug(
+                        " - %s/%s/%s: Build requested.",
+                        self.owner.name, self.name, arch.architecturetag)
+                builds.append(build)
+            except SnapBuildAlreadyPending as e:
+                if logger is not None:
+                    logger.warning(
+                        " - %s/%s/%s: %s",
+                        self.owner.name, self.name, arch.architecturetag, e)
+            except Exception as e:
+                if logger is not None:
+                    logger.exception(
+                        " - %s/%s/%s: %s",
+                        self.owner.name, self.name, arch.architecturetag, e)
+        return builds
+
     def getBuildSummariesForSnapBuildIds(self, snap_build_ids):
         """See `ISnap`."""
         result = {}
@@ -720,33 +756,7 @@
         snaps = cls._findStaleSnaps()
         builds = []
         for snap in snaps:
-            snap.is_stale = False
-            if logger is not None:
-                logger.debug(
-                    "Scheduling builds of snap package %s/%s",
-                    snap.owner.name, snap.name)
-            for arch in snap.getAllowedArchitectures():
-                try:
-                    build = snap.requestBuild(
-                        snap.owner, snap.auto_build_archive, arch,
-                        snap.auto_build_pocket)
-                    if logger is not None:
-                        logger.debug(
-                            " - %s/%s/%s: Build requested.",
-                            snap.owner.name, snap.name, arch.architecturetag)
-                    builds.append(build)
-                except SnapBuildAlreadyPending as e:
-                    if logger is not None:
-                        logger.warning(
-                            " - %s/%s/%s: %s",
-                            snap.owner.name, snap.name, arch.architecturetag,
-                            e)
-                except Exception as e:
-                    if logger is not None:
-                        logger.exception(
-                            " - %s/%s/%s: %s",
-                            snap.owner.name, snap.name, arch.architecturetag,
-                            e)
+            builds.extend(snap.requestAutoBuilds(logger=logger))
         return builds
 
     def detachFromBranch(self, branch):

=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py	2016-07-01 11:53:31 +0000
+++ lib/lp/snappy/tests/test_snap.py	2016-07-18 17:19:30 +0000
@@ -16,6 +16,7 @@
 from storm.locals import Store
 from testtools.matchers import (
     Equals,
+    MatchesSetwise,
     MatchesStructure,
     )
 import transaction
@@ -305,6 +306,30 @@
             snap.owner, snap.distro_series.main_archive, distroarchseries,
             PackagePublishingPocket.UPDATES)
 
+    def test_requestAutoBuilds(self):
+        # requestAutoBuilds creates new builds for all configured
+        # architectures with appropriate parameters.
+        distroseries = self.factory.makeDistroSeries()
+        dases = []
+        for _ in range(3):
+            processor = self.factory.makeProcessor(supports_virtualized=True)
+            dases.append(self.makeBuildableDistroArchSeries(
+                distroseries=distroseries, processor=processor))
+        archive = self.factory.makeArchive()
+        snap = self.factory.makeSnap(
+            distroseries=distroseries,
+            processors=[das.processor for das in dases[:2]],
+            auto_build_archive=archive,
+            auto_build_pocket=PackagePublishingPocket.PROPOSED)
+        with person_logged_in(snap.owner):
+            builds = snap.requestAutoBuilds()
+        self.assertThat(builds, MatchesSetwise(
+            *(MatchesStructure.byEquality(
+                requester=snap.owner, snap=snap, archive=archive,
+                distro_arch_series=das,
+                pocket=PackagePublishingPocket.PROPOSED)
+              for das in dases[:2])))
+
     def test_getBuilds(self):
         # Test the various getBuilds methods.
         snap = self.factory.makeSnap()
@@ -1063,7 +1088,8 @@
 
     def makeSnap(self, owner=None, distroseries=None, branch=None,
                  git_ref=None, processors=None, webservice=None,
-                 private=False):
+                 private=False, auto_build_archive=None,
+                 auto_build_pocket=None):
         if owner is None:
             owner = self.person
         if distroseries is None:
@@ -1083,6 +1109,10 @@
         if processors is not None:
             kwargs["processors"] = [
                 api_url(processor) for processor in processors]
+        if auto_build_archive is not None:
+            kwargs["auto_build_archive"] = api_url(auto_build_archive)
+        if auto_build_pocket is not None:
+            kwargs["auto_build_pocket"] = auto_build_pocket.title
         logout()
         response = webservice.named_post(
             "/+snaps", "new", owner=owner_url, distro_series=distroseries_url,
@@ -1425,6 +1455,50 @@
             "if the snap package owner and the archive owner are equal.",
             response.body)
 
+    def test_requestAutoBuilds(self):
+        # requestAutoBuilds can be performed over the webservice.
+        distroseries = self.factory.makeDistroSeries()
+        dases = []
+        for _ in range(3):
+            processor = self.factory.makeProcessor(supports_virtualized=True)
+            dases.append(self.makeBuildableDistroArchSeries(
+                distroseries=distroseries, processor=processor))
+        archive = self.factory.makeArchive()
+        snap = self.makeSnap(
+            distroseries=distroseries,
+            processors=[das.processor for das in dases[:2]],
+            auto_build_archive=archive,
+            auto_build_pocket=PackagePublishingPocket.PROPOSED)
+        response = self.webservice.named_post(
+            snap["self_link"], "requestAutoBuilds")
+        self.assertEqual(200, response.status)
+        builds = response.jsonBody()
+        self.assertContentEqual(
+            [self.getURL(das) for das in dases[:2]],
+            [build["distro_arch_series_link"] for build in builds])
+
+    def test_requestAutoBuilds_requires_auto_build_archive(self):
+        # requestAutoBuilds fails if auto_build_archive is not set.
+        snap = self.makeSnap()
+        response = self.webservice.named_post(
+            snap["self_link"], "requestAutoBuilds")
+        self.assertEqual(400, response.status)
+        self.assertEqual(
+            "This snap package cannot have automatic builds created for it "
+            "because auto_build_archive is not set.",
+            response.body)
+
+    def test_requestAutoBuilds_requires_auto_build_pocket(self):
+        # requestAutoBuilds fails if auto_build_pocket is not set.
+        snap = self.makeSnap(auto_build_archive=self.factory.makeArchive())
+        response = self.webservice.named_post(
+            snap["self_link"], "requestAutoBuilds")
+        self.assertEqual(400, response.status)
+        self.assertEqual(
+            "This snap package cannot have automatic builds created for it "
+            "because auto_build_pocket is not set.",
+            response.body)
+
     def test_getBuilds(self):
         # The builds, completed_builds, and pending_builds properties are as
         # expected.


Follow ups