launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #22632
[Merge] lp:~cjwatson/launchpad/snap-request-builds-job into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-request-builds-job into lp:launchpad with lp:~cjwatson/launchpad/snap-parse-architectures as a prerequisite.
Commit message:
Add a job to request builds of a snap for relevant architectures.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1770400 in Launchpad itself: "Support snapcraft architectures keyword"
https://bugs.launchpad.net/launchpad/+bug/1770400
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-request-builds-job/+merge/348058
See also https://code.launchpad.net/~cjwatson/launchpad/db-snap-job/+merge/348057.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-request-builds-job into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2018-05-22 06:42:35 +0000
+++ database/schema/security.cfg 2018-06-15 13:00:33 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
#
# Possible permissions: SELECT, INSERT, UPDATE, EXECUTE
@@ -287,6 +287,7 @@
public.snapbuild = SELECT, INSERT, UPDATE, DELETE
public.snapbuildjob = SELECT, INSERT, UPDATE, DELETE
public.snapfile = SELECT, INSERT, UPDATE, DELETE
+public.snapjob = SELECT, INSERT, UPDATE, DELETE
public.snappydistroseries = SELECT, INSERT, UPDATE, DELETE
public.snappyseries = SELECT, INSERT, UPDATE, DELETE
public.sourcepackageformatselection = SELECT
@@ -2537,21 +2538,33 @@
groups=script
public.account = SELECT
public.archive = SELECT
+public.branch = SELECT
public.builder = SELECT
+public.buildfarmjob = SELECT, INSERT
+public.buildqueue = SELECT, INSERT, UPDATE
public.distribution = SELECT
public.distroarchseries = SELECT
public.distroseries = SELECT
public.emailaddress = SELECT
+public.gitref = SELECT
+public.gitrepository = SELECT
public.job = SELECT, INSERT, UPDATE
public.libraryfilealias = SELECT
public.libraryfilecontent = SELECT
public.person = SELECT
public.personsettings = SELECT
+public.pocketchroot = SELECT
+public.processor = SELECT
+public.product = SELECT
public.snap = SELECT, UPDATE
-public.snapbuild = SELECT, UPDATE
+public.snaparch = SELECT
+public.snapbuild = SELECT, INSERT, UPDATE
public.snapbuildjob = SELECT, UPDATE
public.snapfile = SELECT
+public.snapjob = SELECT, UPDATE
public.snappyseries = SELECT
+public.sourcepackagename = SELECT
public.teammembership = SELECT
+public.teamparticipation = SELECT
public.webhook = SELECT
public.webhookjob = SELECT, INSERT
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf 2018-06-15 13:00:33 +0000
+++ lib/lp/services/config/schema-lazr.conf 2018-06-15 13:00:33 +0000
@@ -1860,6 +1860,7 @@
IRemoveArtifactSubscriptionsJobSource,
ISelfRenewalNotificationJobSource,
ISevenDayCommercialExpirationJobSource,
+ ISnapRequestBuildsJobSource,
ISnapStoreUploadJobSource,
ITeamInvitationNotificationJobSource,
ITeamJoinNotificationJobSource,
@@ -2002,6 +2003,11 @@
dbuser: product-job
crontab_group: MAIN
+[ISnapRequestBuildsJobSource]
+module: lp.snappy.interfaces.snapjob
+dbuser: snap-build-job
+crontab_group: MAIN
+
[ISnapStoreUploadJobSource]
module: lp.snappy.interfaces.snapbuildjob
dbuser: snap-build-job
=== modified file 'lib/lp/snappy/configure.zcml'
--- lib/lp/snappy/configure.zcml 2017-03-20 00:03:52 +0000
+++ lib/lp/snappy/configure.zcml 2018-06-15 13:00:33 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2015-2017 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2015-2018 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -127,6 +127,18 @@
</securedutility>
<!-- Snap-related jobs -->
+ <class class="lp.snappy.model.snapjob.SnapJob">
+ <allow interface="lp.snappy.interfaces.snapjob.ISnapJob" />
+ </class>
+ <securedutility
+ component="lp.snappy.model.snapjob.SnapRequestBuildsJob"
+ provides="lp.snappy.interfaces.snapjob.ISnapRequestBuildsJobSource">
+ <allow interface="lp.snappy.interfaces.snapjob.ISnapRequestBuildsJobSource" />
+ </securedutility>
+ <class class="lp.snappy.model.snapjob.SnapRequestBuildsJob">
+ <allow interface="lp.snappy.interfaces.snapjob.ISnapJob" />
+ <allow interface="lp.snappy.interfaces.snapjob.ISnapRequestBuildsJob" />
+ </class>
<class class="lp.snappy.model.snapbuildjob.SnapBuildJob">
<allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" />
</class>
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2018-06-15 13:00:33 +0000
+++ lib/lp/snappy/interfaces/snap.py 2018-06-15 13:00:33 +0000
@@ -316,6 +316,21 @@
:return: `ISnapBuild`.
"""
+ def requestBuildsFromJob(requester, archive, pocket, channels=None,
+ logger=None):
+ """Synchronous part of `Snap.requestBuilds`.
+
+ Request that the snap package be built for relevant architectures.
+
+ :param requester: The person requesting the builds.
+ :param archive: The IArchive to associate the builds with.
+ :param pocket: The pocket that should be targeted.
+ :param channels: A dictionary mapping snap names to channels to use
+ for these builds.
+ :param logger: An optional logger.
+ :return: A sequence of `ISnapBuild` instances.
+ """
+
@operation_parameters(
snap_build_ids=List(
title=_("A list of snap build ids."),
=== added file 'lib/lp/snappy/interfaces/snapjob.py'
--- lib/lp/snappy/interfaces/snapjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/interfaces/snapjob.py 2018-06-15 13:00:33 +0000
@@ -0,0 +1,97 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Snap job interfaces."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'ISnapJob',
+ 'ISnapRequestBuildsJob',
+ 'ISnapRequestBuildsJobSource',
+ ]
+
+from lazr.restful.fields import Reference
+from zope.interface import (
+ Attribute,
+ Interface,
+ )
+from zope.schema import (
+ Choice,
+ Dict,
+ List,
+ TextLine,
+ )
+
+from lp import _
+from lp.registry.interfaces.person import IPerson
+from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.services.job.interfaces.job import (
+ IJob,
+ IJobSource,
+ IRunnableJob,
+ )
+from lp.snappy.interfaces.snap import ISnap
+from lp.snappy.interfaces.snapbuild import ISnapBuild
+from lp.soyuz.interfaces.archive import IArchive
+
+
+class ISnapJob(Interface):
+ """A job related to a snap package."""
+
+ job = Reference(
+ title=_("The common Job attributes."), schema=IJob,
+ required=True, readonly=True)
+
+ snap = Reference(
+ title=_("The snap package to use for this job."),
+ schema=ISnap, required=True, readonly=True)
+
+ metadata = Attribute(_("A dict of data about the job."))
+
+
+class ISnapRequestBuildsJob(IRunnableJob):
+ """A Job that processes a request for builds of a snap package."""
+
+ requester = Reference(
+ title=_("The person requesting the builds."), schema=IPerson,
+ required=True, readonly=True)
+
+ archive = Reference(
+ title=_("The archive to associate the builds with."), schema=IArchive,
+ required=True, readonly=True)
+
+ pocket = Choice(
+ title=_("The pocket that should be targeted."),
+ vocabulary=PackagePublishingPocket, required=True, readonly=True)
+
+ channels = Dict(
+ title=_("Source snap channels to use for these builds."),
+ description=_(
+ "A dictionary mapping snap names to channels to use for these "
+ "builds. Currently only 'core' and 'snapcraft' keys are "
+ "supported."),
+ key_type=TextLine(), required=False, readonly=True)
+
+ error_message = TextLine(
+ title=_("Error message resulting from running this job."),
+ required=False, readonly=True)
+
+ builds = List(
+ title=_("The builds created by this request."),
+ value_type=Reference(schema=ISnapBuild), required=True, readonly=True)
+
+
+class ISnapRequestBuildsJobSource(IJobSource):
+
+ def create(snap, requester, archive, pocket, channels):
+ """Request builds of a snap package.
+
+ :param snap: The snap package to build.
+ :param requester: The person requesting the builds.
+ :param archive: The IArchive to associate the builds with.
+ :param pocket: The pocket that should be targeted.
+ :param channels: A dictionary mapping snap names to channels to use
+ for these builds.
+ """
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2018-06-15 13:00:33 +0000
+++ lib/lp/snappy/model/snap.py 2018-06-15 13:00:33 +0000
@@ -6,10 +6,12 @@
'Snap',
]
+from collections import OrderedDict
from datetime import (
datetime,
timedelta,
)
+from operator import attrgetter
from urlparse import urlsplit
from pymacaroons import Macaroon
@@ -116,6 +118,7 @@
from lp.services.webapp.interfaces import ILaunchBag
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.snap import (
BadSnapSearchContext,
BadSnapSource,
@@ -463,21 +466,25 @@
return False
return True
- def requestBuild(self, requester, archive, distro_arch_series, pocket,
- channels=None):
- """See `ISnap`."""
+ def _checkRequestBuild(self, requester, archive):
+ """May `requester` request builds of this snap from `archive`?"""
if not requester.inTeam(self.owner):
raise SnapNotOwner(
"%s cannot create snap package builds owned by %s." %
(requester.displayname, self.owner.displayname))
if not archive.enabled:
raise ArchiveDisabled(archive.displayname)
- if distro_arch_series not in self.getAllowedArchitectures():
- raise SnapBuildDisallowedArchitecture(distro_arch_series)
if archive.private and self.owner != archive.owner:
# See rationale in `SnapBuildArchiveOwnerMismatch` docstring.
raise SnapBuildArchiveOwnerMismatch()
+ def requestBuild(self, requester, archive, distro_arch_series, pocket,
+ channels=None):
+ """See `ISnap`."""
+ self._checkRequestBuild(requester, archive)
+ if distro_arch_series not in self.getAllowedArchitectures():
+ raise SnapBuildDisallowedArchitecture(distro_arch_series)
+
pending = IStore(self).find(
SnapBuild,
SnapBuild.snap_id == self.id,
@@ -495,8 +502,42 @@
build.queueBuild()
return build
+ def requestBuildsFromJob(self, requester, archive, pocket, channels=None,
+ logger=None):
+ """See `ISnap`."""
+ snapcraft_data = removeSecurityProxy(
+ getUtility(ISnapSet).getSnapcraftYaml(self))
+ # 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(),
+ key=attrgetter("processor.id")))
+ architectures_to_build = determine_architectures_to_build(
+ snapcraft_data, supported_arches.keys())
+
+ builds = []
+ for build_instance in architectures_to_build:
+ arch = build_instance.architecture
+ try:
+ build = self.requestBuild(
+ requester, archive, supported_arches[arch], pocket,
+ channels)
+ if logger is not None:
+ logger.debug(
+ " - %s/%s/%s: Build requested.",
+ self.owner.name, self.name, arch)
+ builds.append(build)
+ except SnapBuildAlreadyPending as e:
+ if logger is not None:
+ logger.warning(
+ " - %s/%s/%s: %s",
+ self.owner.name, self.name, arch, e)
+ return builds
+
def requestAutoBuilds(self, allow_failures=False, logger=None):
- """See `ISnapSet`."""
+ """See `ISnap`."""
builds = []
if self.auto_build_archive is None:
raise CannotRequestAutoBuilds("auto_build_archive")
=== added file 'lib/lp/snappy/model/snapjob.py'
--- lib/lp/snappy/model/snapjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/model/snapjob.py 2018-06-15 13:00:33 +0000
@@ -0,0 +1,271 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Snap package jobs."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'SnapJob',
+ 'SnapJobType',
+ 'SnapRequestBuildsJob',
+ ]
+
+from lazr.delegates import delegate_to
+from lazr.enum import (
+ DBEnumeratedType,
+ DBItem,
+ )
+from storm.locals import (
+ Int,
+ JSON,
+ Reference,
+ )
+from storm.store import EmptyResultSet
+import transaction
+from zope.component import getUtility
+from zope.interface import (
+ implementer,
+ provider,
+ )
+
+from lp.app.errors import NotFoundError
+from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.services.config import config
+from lp.services.database.enumcol import EnumCol
+from lp.services.database.interfaces import (
+ IMasterStore,
+ IStore,
+ )
+from lp.services.database.stormbase import StormBase
+from lp.services.job.model.job import (
+ EnumeratedSubclass,
+ Job,
+ )
+from lp.services.job.runner import BaseRunnableJob
+from lp.services.mail.sendmail import format_address_for_person
+from lp.services.propertycache import cachedproperty
+from lp.services.scripts import log
+from lp.snappy.interfaces.snap import (
+ CannotFetchSnapcraftYaml,
+ CannotParseSnapcraftYaml,
+ )
+from lp.snappy.interfaces.snapjob import (
+ ISnapJob,
+ ISnapRequestBuildsJob,
+ ISnapRequestBuildsJobSource,
+ )
+from lp.snappy.model.snapbuild import SnapBuild
+from lp.soyuz.model.archive import Archive
+
+
+class SnapJobType(DBEnumeratedType):
+ """Values that `ISnapJob.job_type` can take."""
+
+ REQUEST_BUILDS = DBItem(0, """
+ Request builds
+
+ This job requests builds of a snap package.
+ """)
+
+
+@implementer(ISnapJob)
+class SnapJob(StormBase):
+ """See `ISnapJob`."""
+
+ __storm_table__ = 'SnapJob'
+
+ job_id = Int(name='job', primary=True, allow_none=False)
+ job = Reference(job_id, 'Job.id')
+
+ snap_id = Int(name='snap', allow_none=False)
+ snap = Reference(snap_id, 'Snap.id')
+
+ job_type = EnumCol(enum=SnapJobType, notNull=True)
+
+ metadata = JSON('json_data', allow_none=False)
+
+ def __init__(self, snap, job_type, metadata, **job_args):
+ """Constructor.
+
+ Extra keyword arguments are used to construct the underlying Job
+ object.
+
+ :param snap: The `ISnap` this job relates to.
+ :param job_type: The `SnapJobType` of this job.
+ :param metadata: The type-specific variables, as a JSON-compatible
+ dict.
+ """
+ super(SnapJob, self).__init__()
+ self.job = Job(**job_args)
+ self.snap = snap
+ self.job_type = job_type
+ self.metadata = metadata
+
+ def makeDerived(self):
+ return SnapJobDerived.makeSubclass(self)
+
+
+@delegate_to(ISnapJob)
+class SnapJobDerived(BaseRunnableJob):
+
+ __metaclass__ = EnumeratedSubclass
+
+ def __init__(self, snap_job):
+ self.context = snap_job
+
+ def __repr__(self):
+ """An informative representation of the job."""
+ return "<%s for ~%s/+snap/%s>" % (
+ self.__class__.__name__, self.snap.owner.name, self.snap.name)
+
+ @classmethod
+ def get(cls, job_id):
+ """Get a job by id.
+
+ :return: The `SnapJob` with the specified id, as the current
+ `SnapJobDerived` subclass.
+ :raises: `NotFoundError` if there is no job with the specified id,
+ or its `job_type` does not match the desired subclass.
+ """
+ snap_job = IStore(SnapJob).get(SnapJob, job_id)
+ if snap_job.job_type != cls.class_job_type:
+ raise NotFoundError(
+ "No object found with id %d and type %s" %
+ (job_id, cls.class_job_type.title))
+ return cls(snap_job)
+
+ @classmethod
+ def iterReady(cls):
+ """See `IJobSource`."""
+ jobs = IMasterStore(SnapJob).find(
+ SnapJob,
+ SnapJob.job_type == cls.class_job_type,
+ SnapJob.job == Job.id,
+ Job.id.is_in(Job.ready_jobs))
+ return (cls(job) for job in jobs)
+
+ def getOopsVars(self):
+ """See `IRunnableJob`."""
+ oops_vars = super(SnapJobDerived, self).getOopsVars()
+ oops_vars.extend([
+ ("job_id", self.context.job.id),
+ ("job_type", self.context.job_type.title),
+ ("snap_owner_name", self.context.snap.owner.name),
+ ("snap_name", self.context.snap.name),
+ ])
+ return oops_vars
+
+
+@implementer(ISnapRequestBuildsJob)
+@provider(ISnapRequestBuildsJobSource)
+class SnapRequestBuildsJob(SnapJobDerived):
+ """A Job that processes a request for builds of a snap package."""
+
+ class_job_type = SnapJobType.REQUEST_BUILDS
+
+ user_error_types = (CannotParseSnapcraftYaml, NotFoundError)
+ retry_error_types = (CannotFetchSnapcraftYaml,)
+
+ max_retries = 5
+
+ config = config.ISnapRequestBuildsJobSource
+
+ @classmethod
+ def create(cls, snap, requester, archive, pocket, channels):
+ """See `ISnapRequestBuildsJobSource`."""
+ metadata = {
+ "requester": requester.id,
+ "archive": archive.id,
+ "pocket": pocket.value,
+ "channels": channels,
+ }
+ snap_job = SnapJob(snap, cls.class_job_type, metadata)
+ job = cls(snap_job)
+ job.celeryRunOnCommit()
+ return job
+
+ def getOperationDescription(self):
+ return "requesting builds of %s" % self.snap.name
+
+ def getErrorRecipients(self):
+ if self.requester is None or self.requester.preferredemail is None:
+ return []
+ return [format_address_for_person(self.requester)]
+
+ @cachedproperty
+ def requester(self):
+ """See `ISnapRequestBuildsJob`."""
+ requester_id = self.metadata["requester"]
+ return getUtility(IPersonSet).get(requester_id)
+
+ @cachedproperty
+ def archive(self):
+ """See `ISnapRequestBuildsJob`."""
+ archive_id = self.metadata["archive"]
+ return IStore(Archive).find(Archive, Archive.id == archive_id).one()
+
+ @property
+ def pocket(self):
+ """See `ISnapRequestBuildsJob`."""
+ name = self.metadata["pocket"]
+ return PackagePublishingPocket.items[name]
+
+ @property
+ def channels(self):
+ """See `ISnapRequestBuildsJob`."""
+ return self.metadata["channels"]
+
+ @property
+ def error_message(self):
+ """See `ISnapRequestBuildsJob`."""
+ return self.metadata.get("error_message")
+
+ @error_message.setter
+ def error_message(self, message):
+ """See `ISnapRequestBuildsJob`."""
+ self.metadata["error_message"] = message
+
+ @property
+ def builds(self):
+ """See `ISnapRequestBuildsJob`."""
+ build_ids = self.metadata.get("builds")
+ if build_ids is None:
+ return EmptyResultSet()
+ else:
+ return IStore(SnapBuild).find(
+ SnapBuild, SnapBuild.id.is_in(build_ids))
+
+ @builds.setter
+ def builds(self, builds):
+ """See `ISnapRequestBuildsJob`."""
+ self.metadata["builds"] = [build.id for build in builds]
+
+ def run(self):
+ """See `IRunnableJob`."""
+ requester = self.requester
+ if requester is None:
+ log.info(
+ "Skipping %r because the requester has been deleted." % self)
+ return
+ archive = self.archive
+ if archive is None:
+ log.info(
+ "Skipping %r because the archive has been deleted." % self)
+ return
+ try:
+ self.builds = self.snap.requestBuildsFromJob(
+ requester, archive, self.pocket, channels=self.channels,
+ logger=log)
+ self.error_message = None
+ except self.retry_error_types:
+ raise
+ except Exception as e:
+ self.error_message = str(e)
+ # The normal job infrastructure will abort the transaction, but
+ # we want to commit instead: the only database changes we make
+ # are to this job's metadata and should be preserved.
+ transaction.commit()
+ raise
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2018-06-15 13:00:33 +0000
+++ lib/lp/snappy/tests/test_snap.py 2018-06-15 13:00:33 +0000
@@ -12,6 +12,7 @@
timedelta,
)
import json
+from textwrap import dedent
from urlparse import urlsplit
from fixtures import MockPatch
@@ -91,6 +92,7 @@
ISnapBuildSet,
)
from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource
+from lp.snappy.interfaces.snapjob import ISnapRequestBuildsJobSource
from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
from lp.snappy.model.snap import SnapSet
from lp.snappy.model.snapbuild import SnapFile
@@ -361,6 +363,65 @@
snap.owner, snap.distro_series.main_archive, distroarchseries,
PackagePublishingPocket.UPDATES)
+ def makeRequestBuildsJob(self, arch_tags):
+ distro = self.factory.makeDistribution()
+ distroseries = self.factory.makeDistroSeries(distribution=distro)
+ processors = [
+ self.factory.makeProcessor(
+ name=arch_tag, supports_virtualized=True)
+ for arch_tag in arch_tags]
+ for processor in processors:
+ das = self.factory.makeDistroArchSeries(
+ distroseries=distroseries, architecturetag=processor.name,
+ processor=processor)
+ das.addOrUpdateChroot(self.factory.makeLibraryFileAlias(
+ filename="fake_chroot.tar.gz", db_only=True))
+ [git_ref] = self.factory.makeGitRefs()
+ snap = self.factory.makeSnap(
+ git_ref=git_ref, distroseries=distroseries, processors=processors)
+ return getUtility(ISnapRequestBuildsJobSource).create(
+ snap, snap.owner.teamowner, distro.main_archive,
+ PackagePublishingPocket.RELEASE, {"snapcraft": "edge"})
+
+ def assertRequestedBuildsMatch(self, builds, job, arch_tags):
+ 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]),
+ pocket=Equals(job.pocket),
+ channels=Equals(job.channels))
+ for arch_tag in arch_tags)))
+
+ def test_requestBuildsFromJob_restricts_explicit_list(self):
+ # requestBuildsFromJob limits builds targeted at an explicit list of
+ # architectures to those allowed for the snap.
+ self.useFixture(GitHostingFixture(blob=dedent("""\
+ architectures:
+ - build-on: sparc
+ - build-on: i386
+ - build-on: avr
+ """)))
+ job = self.makeRequestBuildsJob(["sparc"])
+ with person_logged_in(job.requester):
+ builds = job.snap.requestBuildsFromJob(
+ job.requester, job.archive, job.pocket,
+ removeSecurityProxy(job.channels))
+ self.assertRequestedBuildsMatch(builds, job, ["sparc"])
+
+ def test_requestBuildsFromJob_no_explicit_architectures(self):
+ # If the snap doesn't specify any architectures,
+ # requestBuildsFromJob requests builds for all configured
+ # architectures.
+ self.useFixture(GitHostingFixture(blob="name: foo\n"))
+ job = self.makeRequestBuildsJob(["mips64el", "riscv64"])
+ with person_logged_in(job.requester):
+ builds = job.snap.requestBuildsFromJob(
+ job.requester, job.archive, job.pocket,
+ removeSecurityProxy(job.channels))
+ self.assertRequestedBuildsMatch(builds, job, ["mips64el", "riscv64"])
+
def test_requestAutoBuilds(self):
# requestAutoBuilds creates new builds for all configured
# architectures with appropriate parameters.
=== added file 'lib/lp/snappy/tests/test_snapjob.py'
--- lib/lp/snappy/tests/test_snapjob.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_snapjob.py 2018-06-15 13:00:33 +0000
@@ -0,0 +1,155 @@
+# Copyright 2018 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for snap package jobs."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from textwrap import dedent
+
+from testtools.matchers import (
+ AfterPreprocessing,
+ ContainsDict,
+ Equals,
+ Is,
+ MatchesSetwise,
+ MatchesStructure,
+ )
+
+from lp.code.tests.helpers import GitHostingFixture
+from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.services.config import config
+from lp.services.job.interfaces.job import JobStatus
+from lp.services.job.runner import JobRunner
+from lp.services.mail.sendmail import format_address_for_person
+from lp.snappy.interfaces.snap import CannotParseSnapcraftYaml
+from lp.snappy.interfaces.snapjob import (
+ ISnapJob,
+ ISnapRequestBuildsJob,
+ )
+from lp.snappy.model.snapjob import (
+ SnapJob,
+ SnapJobType,
+ SnapRequestBuildsJob,
+ )
+from lp.testing import TestCaseWithFactory
+from lp.testing.dbuser import dbuser
+from lp.testing.layers import ZopelessDatabaseLayer
+
+
+class TestSnapJob(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_provides_interface(self):
+ # `SnapJob` objects provide `ISnapJob`.
+ snap = self.factory.makeSnap()
+ self.assertProvides(
+ SnapJob(snap, SnapJobType.REQUEST_BUILDS, {}), ISnapJob)
+
+
+class TestSnapRequestBuildsJob(TestCaseWithFactory):
+
+ layer = ZopelessDatabaseLayer
+
+ def test_provides_interface(self):
+ # `SnapRequestBuildsJob` objects provide `ISnapRequestBuildsJob`."""
+ snap = self.factory.makeSnap()
+ archive = self.factory.makeArchive()
+ job = SnapRequestBuildsJob.create(
+ snap, snap.registrant, archive, PackagePublishingPocket.RELEASE,
+ None)
+ self.assertProvides(job, ISnapRequestBuildsJob)
+
+ def test___repr__(self):
+ # `SnapRequestBuildsJob` objects have an informative __repr__.
+ snap = self.factory.makeSnap()
+ archive = self.factory.makeArchive()
+ job = SnapRequestBuildsJob.create(
+ snap, snap.registrant, archive, PackagePublishingPocket.RELEASE,
+ None)
+ self.assertEqual(
+ "<SnapRequestBuildsJob for ~%s/+snap/%s>" % (
+ snap.owner.name, snap.name),
+ repr(job))
+
+ def makeSeriesAndProcessors(self, arch_tags):
+ distro = self.factory.makeDistribution()
+ distroseries = self.factory.makeDistroSeries(distribution=distro)
+ processors = [
+ self.factory.makeProcessor(
+ name=arch_tag, supports_virtualized=True)
+ for arch_tag in arch_tags]
+ for processor in processors:
+ das = self.factory.makeDistroArchSeries(
+ distroseries=distroseries, architecturetag=processor.name,
+ processor=processor)
+ das.addOrUpdateChroot(self.factory.makeLibraryFileAlias(
+ filename="fake_chroot.tar.gz", db_only=True))
+ return distroseries, processors
+
+ def test_run(self):
+ # The job requests builds and records the result.
+ distroseries, processors = self.makeSeriesAndProcessors(
+ ["avr2001", "sparc64", "x32"])
+ [git_ref] = self.factory.makeGitRefs()
+ snap = self.factory.makeSnap(
+ git_ref=git_ref, distroseries=distroseries, processors=processors)
+ job = SnapRequestBuildsJob.create(
+ snap, snap.registrant, distroseries.main_archive,
+ PackagePublishingPocket.RELEASE, {"core": "stable"})
+ snapcraft_yaml = dedent("""\
+ architectures:
+ - build-on: avr2001
+ - build-on: x32
+ """)
+ self.useFixture(GitHostingFixture(blob=snapcraft_yaml))
+ with dbuser(config.ISnapRequestBuildsJobSource.dbuser):
+ JobRunner([job]).runAll()
+ self.assertEmailQueueLength(0)
+ self.assertThat(job, MatchesStructure(
+ job=MatchesStructure.byEquality(status=JobStatus.COMPLETED),
+ error_message=Is(None),
+ builds=AfterPreprocessing(set, MatchesSetwise(*[
+ MatchesStructure.byEquality(
+ requester=snap.registrant,
+ snap=snap,
+ archive=distroseries.main_archive,
+ distro_arch_series=distroseries[arch],
+ pocket=PackagePublishingPocket.RELEASE,
+ channels={"core": "stable"})
+ for arch in ("avr2001", "x32")]))))
+
+ def test_run_failed(self):
+ # A failed run sets the job status to FAILED and records the error
+ # message.
+ # The job requests builds and records the result.
+ distroseries, processors = self.makeSeriesAndProcessors(
+ ["avr2001", "sparc64", "x32"])
+ [git_ref] = self.factory.makeGitRefs()
+ snap = self.factory.makeSnap(
+ git_ref=git_ref, distroseries=distroseries, processors=processors)
+ job = SnapRequestBuildsJob.create(
+ snap, snap.registrant, distroseries.main_archive,
+ PackagePublishingPocket.RELEASE, {"core": "stable"})
+ self.useFixture(GitHostingFixture()).getBlob.failure = (
+ CannotParseSnapcraftYaml("Nonsense on stilts"))
+ with dbuser(config.ISnapRequestBuildsJobSource.dbuser):
+ JobRunner([job]).runAll()
+ [notification] = self.assertEmailQueueLength(1)
+ self.assertThat(dict(notification), ContainsDict({
+ "From": Equals(config.canonical.noreply_from_address),
+ "To": Equals(format_address_for_person(snap.registrant)),
+ "Subject": Equals(
+ "Launchpad error while requesting builds of %s" % snap.name),
+ }))
+ self.assertEqual(
+ "Launchpad encountered an error during the following operation: "
+ "requesting builds of %s. Nonsense on stilts" % snap.name,
+ notification.get_payload(decode=True))
+ self.assertThat(job, MatchesStructure(
+ job=MatchesStructure.byEquality(status=JobStatus.FAILED),
+ error_message=Equals("Nonsense on stilts"),
+ builds=AfterPreprocessing(set, MatchesSetwise())))
Follow ups