launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19063
[Merge] lp:~cjwatson/launchpad/snapbuild-basic-model into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snapbuild-basic-model into lp:launchpad with lp:~cjwatson/launchpad/snap-basic-model as a prerequisite.
Commit message:
Add basic model for snap builds.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1476405 in Launchpad itself: "Add support for building snaps"
https://bugs.launchpad.net/launchpad/+bug/1476405
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snapbuild-basic-model/+merge/265691
Add basic model for snap builds. The links between Snap and SnapBuild aren't in place yet for the sake of branch size, but that will come next; this completes the core database modelling.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snapbuild-basic-model into lp:launchpad.
=== modified file 'lib/lp/buildmaster/enums.py'
--- lib/lp/buildmaster/enums.py 2014-06-20 11:12:40 +0000
+++ lib/lp/buildmaster/enums.py 2015-07-23 16:10:25 +0000
@@ -157,6 +157,12 @@
Build a live filesystem from an archive.
""")
+ SNAPBUILD = DBItem(6, """
+ Snap package build
+
+ Build a snap package from a recipe.
+ """)
+
class BuildQueueStatus(DBEnumeratedType):
"""Build queue status.
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2015-07-23 16:10:25 +0000
+++ lib/lp/security.py 2015-07-23 16:10:25 +0000
@@ -192,6 +192,7 @@
ILanguageSet,
)
from lp.snappy.interfaces.snap import ISnap
+from lp.snappy.interfaces.snapbuild import ISnapBuild
from lp.soyuz.interfaces.archive import IArchive
from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken
from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
@@ -3116,3 +3117,33 @@
return (
user.in_ppa_self_admins
and EditSnap(self.obj).checkAuthenticated(user))
+
+
+class ViewSnapBuild(DelegatedAuthorization):
+ permission = 'launchpad.View'
+ usedfor = ISnapBuild
+
+ def iter_objects(self):
+ yield self.obj.snap
+ yield self.obj.archive
+
+
+class EditSnapBuild(AdminByBuilddAdmin):
+ permission = 'launchpad.Edit'
+ usedfor = ISnapBuild
+
+ def checkAuthenticated(self, user):
+ """Check edit access for snap package builds.
+
+ Allow admins, buildd admins, and the owner of the snap package.
+ (Note that the requester of the build is required to be in the team
+ that owns the snap package.)
+ """
+ auth_snap = EditSnap(self.obj.snap)
+ if auth_snap.checkAuthenticated(user):
+ return True
+ return super(EditSnapBuild, self).checkAuthenticated(user)
+
+
+class AdminSnapBuild(AdminByBuilddAdmin):
+ usedfor = ISnapBuild
=== modified file 'lib/lp/snappy/browser/configure.zcml'
--- lib/lp/snappy/browser/configure.zcml 2015-07-23 16:10:25 +0000
+++ lib/lp/snappy/browser/configure.zcml 2015-07-23 16:10:25 +0000
@@ -13,5 +13,15 @@
for="lp.snappy.interfaces.snap.ISnap"
path_expression="string:+snap/${name}"
attribute_to_parent="owner" />
+ <browser:navigation
+ module="lp.snappy.browser.snap"
+ classes="SnapNavigation" />
+ <browser:url
+ for="lp.snappy.interfaces.snapbuild.ISnapBuild"
+ path_expression="string:+build/${id}"
+ attribute_to_parent="snap" />
+ <browser:navigation
+ module="lp.snappy.browser.snapbuild"
+ classes="SnapBuildNavigation" />
</facet>
</configure>
=== added file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/snap.py 2015-07-23 16:10:25 +0000
@@ -0,0 +1,28 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Snap views."""
+
+__metaclass__ = type
+__all__ = [
+ 'SnapNavigation',
+ ]
+
+from lp.services.webapp import (
+ Navigation,
+ stepthrough,
+ )
+from lp.snappy.interfaces.snap import ISnap
+from lp.snappy.interfaces.snapbuild import ISnapBuildSet
+from lp.soyuz.browser.build import get_build_by_id_str
+
+
+class SnapNavigation(Navigation):
+ usedfor = ISnap
+
+ @stepthrough('+build')
+ def traverse_build(self, name):
+ build = get_build_by_id_str(ISnapBuildSet, name)
+ if build is None or build.snap != self.context:
+ return None
+ return build
=== added file 'lib/lp/snappy/browser/snapbuild.py'
--- lib/lp/snappy/browser/snapbuild.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/snapbuild.py 2015-07-23 16:10:25 +0000
@@ -0,0 +1,17 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""SnapBuild views."""
+
+__metaclass__ = type
+__all__ = [
+ 'SnapBuildNavigation',
+ ]
+
+from lp.services.librarian.browser import FileNavigationMixin
+from lp.services.webapp import Navigation
+from lp.snappy.interfaces.snapbuild import ISnapBuild
+
+
+class SnapBuildNavigation(Navigation, FileNavigationMixin):
+ usedfor = ISnapBuild
=== modified file 'lib/lp/snappy/configure.zcml'
--- lib/lp/snappy/configure.zcml 2015-07-23 16:10:25 +0000
+++ lib/lp/snappy/configure.zcml 2015-07-23 16:10:25 +0000
@@ -39,4 +39,35 @@
<allow interface="lp.snappy.interfaces.snap.ISnapSet" />
</securedutility>
+ <!-- SnapBuild -->
+ <class class="lp.snappy.model.snapbuild.SnapBuild">
+ <require
+ permission="launchpad.View"
+ interface="lp.snappy.interfaces.snapbuild.ISnapBuildView" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.snappy.interfaces.snapbuild.ISnapBuildEdit" />
+ <require
+ permission="launchpad.Admin"
+ interface="lp.snappy.interfaces.snapbuild.ISnapBuildAdmin" />
+ </class>
+
+ <!-- SnapBuildSet -->
+ <securedutility
+ class="lp.snappy.model.snapbuild.SnapBuildSet"
+ provides="lp.snappy.interfaces.snapbuild.ISnapBuildSet">
+ <allow interface="lp.snappy.interfaces.snapbuild.ISnapBuildSet" />
+ </securedutility>
+ <securedutility
+ class="lp.snappy.model.snapbuild.SnapBuildSet"
+ provides="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource"
+ name="SNAPBUILD">
+ <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" />
+ </securedutility>
+
+ <!-- SnapFile -->
+ <class class="lp.snappy.model.snapbuild.SnapFile">
+ <allow interface="lp.snappy.interfaces.snapbuild.ISnapFile" />
+ </class>
+
</configure>
=== added file 'lib/lp/snappy/interfaces/snapbuild.py'
--- lib/lp/snappy/interfaces/snapbuild.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/interfaces/snapbuild.py 2015-07-23 16:10:25 +0000
@@ -0,0 +1,161 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Snap package build interfaces."""
+
+__metaclass__ = type
+
+__all__ = [
+ 'ISnapBuild',
+ 'ISnapBuildSet',
+ 'ISnapFile',
+ ]
+
+from lazr.restful.fields import Reference
+from zope.interface import (
+ Attribute,
+ Interface,
+ )
+from zope.schema import (
+ Bool,
+ Choice,
+ Int,
+ )
+
+from lp import _
+from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource
+from lp.buildmaster.interfaces.packagebuild import IPackageBuild
+from lp.registry.interfaces.person import IPerson
+from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.services.database.constants import DEFAULT
+from lp.services.librarian.interfaces import ILibraryFileAlias
+from lp.snappy.interfaces.snap import ISnap
+from lp.soyuz.interfaces.archive import IArchive
+from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
+
+
+class ISnapFile(Interface):
+ """A file produced by a snap package build."""
+
+ snapbuild = Attribute("The snap package build producing this file.")
+
+ libraryfile = Reference(
+ ILibraryFileAlias, title=_("The library file alias for this file."),
+ required=True, readonly=True)
+
+
+class ISnapBuildView(IPackageBuild):
+ """`ISnapBuild` attributes that require launchpad.View permission."""
+
+ requester = Reference(
+ IPerson,
+ title=_("The person who requested this build."),
+ required=True, readonly=True)
+
+ snap = Reference(
+ ISnap,
+ title=_("The snap package to build."),
+ required=True, readonly=True)
+
+ archive = Reference(
+ IArchive,
+ title=_("The archive from which to build the snap package."),
+ required=True, readonly=True)
+
+ distro_arch_series = Reference(
+ IDistroArchSeries,
+ title=_("The series and architecture for which to build."),
+ required=True, readonly=True)
+
+ pocket = Choice(
+ title=_("The pocket for which to build."),
+ vocabulary=PackagePublishingPocket, required=True, readonly=True)
+
+ virtualized = Bool(
+ title=_("If True, this build is virtualized."), readonly=True)
+
+ score = Int(
+ title=_("Score of the related build farm job (if any)."),
+ required=False, readonly=True)
+
+ can_be_rescored = Bool(
+ title=_("Can be rescored"),
+ required=True, readonly=True,
+ description=_("Whether this build record can be rescored manually."))
+
+ can_be_cancelled = Bool(
+ title=_("Can be cancelled"),
+ required=True, readonly=True,
+ description=_("Whether this build record can be cancelled."))
+
+ def getFiles():
+ """Retrieve the build's `ISnapFile` records.
+
+ :return: A result set of (`ISnapFile`, `ILibraryFileAlias`,
+ `ILibraryFileContent`).
+ """
+
+ def getFileByName(filename):
+ """Return the corresponding `ILibraryFileAlias` in this context.
+
+ The following file types (and extension) can be looked up:
+
+ * Build log: '.txt.gz'
+ * Upload log: '_log.txt'
+
+ Any filename not matching one of these extensions is looked up as a
+ snap package output file.
+
+ :param filename: The filename to look up.
+ :raises NotFoundError: if no file exists with the given name.
+ :return: The corresponding `ILibraryFileAlias`.
+ """
+
+ def getFileUrls():
+ """URLs for all the files produced by this build.
+
+ :return: A collection of URLs for this build."""
+
+
+class ISnapBuildEdit(Interface):
+ """`ISnapBuild` attributes that require launchpad.Edit."""
+
+ def addFile(lfa):
+ """Add a file to this build.
+
+ :param lfa: An `ILibraryFileAlias`.
+ :return: An `ISnapFile`.
+ """
+
+ def cancel():
+ """Cancel the build if it is either pending or in progress.
+
+ Check the can_be_cancelled property prior to calling this method to
+ find out if cancelling the build is possible.
+
+ If the build is in progress, it is marked as CANCELLING until the
+ buildd manager terminates the build and marks it CANCELLED. If the
+ build is not in progress, it is marked CANCELLED immediately and is
+ removed from the build queue.
+
+ If the build is not in a cancellable state, this method is a no-op.
+ """
+
+
+class ISnapBuildAdmin(Interface):
+ """`ISnapBuild` attributes that require launchpad.Admin."""
+
+ def rescore(score):
+ """Change the build's score."""
+
+
+class ISnapBuild(ISnapBuildView, ISnapBuildEdit, ISnapBuildAdmin):
+ """Build information for snap package builds."""
+
+
+class ISnapBuildSet(ISpecificBuildFarmJobSource):
+ """Utility for `ISnapBuild`."""
+
+ def new(requester, snap, archive, distro_arch_series, pocket,
+ date_created=DEFAULT):
+ """Create an `ISnapBuild`."""
=== added file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/model/snapbuild.py 2015-07-23 16:10:25 +0000
@@ -0,0 +1,357 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+__all__ = [
+ 'SnapBuild',
+ 'SnapFile',
+ ]
+
+from datetime import timedelta
+
+import pytz
+from storm.locals import (
+ Bool,
+ DateTime,
+ Desc,
+ Int,
+ Reference,
+ Store,
+ Storm,
+ Unicode,
+ )
+from storm.store import EmptyResultSet
+from zope.component import getUtility
+from zope.interface import implementer
+
+from lp.app.errors import NotFoundError
+from lp.buildmaster.enums import (
+ BuildFarmJobType,
+ BuildStatus,
+ )
+from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
+from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin
+from lp.buildmaster.model.packagebuild import PackageBuildMixin
+from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.registry.model.person import Person
+from lp.services.database.bulk import load_related
+from lp.services.database.constants import DEFAULT
+from lp.services.database.decoratedresultset import DecoratedResultSet
+from lp.services.database.enumcol import DBEnum
+from lp.services.database.interfaces import (
+ IMasterStore,
+ IStore,
+ )
+from lp.services.features import getFeatureFlag
+from lp.services.librarian.browser import ProxiedLibraryFileAlias
+from lp.services.librarian.model import (
+ LibraryFileAlias,
+ LibraryFileContent,
+ )
+from lp.snappy.interfaces.snap import (
+ SNAP_FEATURE_FLAG,
+ SnapFeatureDisabled,
+ )
+from lp.snappy.interfaces.snapbuild import (
+ ISnapBuild,
+ ISnapBuildSet,
+ ISnapFile,
+ )
+from lp.soyuz.interfaces.component import IComponentSet
+from lp.soyuz.model.archive import Archive
+
+
+@implementer(ISnapFile)
+class SnapFile(Storm):
+ """See `ISnap`."""
+
+ __storm_table__ = 'SnapFile'
+
+ id = Int(name='id', primary=True)
+
+ snapbuild_id = Int(name='snapbuild', allow_none=False)
+ snapbuild = Reference(snapbuild_id, 'SnapBuild.id')
+
+ libraryfile_id = Int(name='libraryfile', allow_none=False)
+ libraryfile = Reference(libraryfile_id, 'LibraryFileAlias.id')
+
+ def __init__(self, snapbuild, libraryfile):
+ """Construct a `SnapFile`."""
+ super(SnapFile, self).__init__()
+ self.snapbuild = snapbuild
+ self.libraryfile = libraryfile
+
+
+@implementer(ISnapBuild)
+class SnapBuild(PackageBuildMixin, Storm):
+ """See `ISnapBuild`."""
+
+ __storm_table__ = 'SnapBuild'
+
+ job_type = BuildFarmJobType.SNAPBUILD
+
+ id = Int(name='id', primary=True)
+
+ build_farm_job_id = Int(name='build_farm_job', allow_none=False)
+ build_farm_job = Reference(build_farm_job_id, 'BuildFarmJob.id')
+
+ requester_id = Int(name='requester', allow_none=False)
+ requester = Reference(requester_id, 'Person.id')
+
+ snap_id = Int(name='snap', allow_none=False)
+ snap = Reference(snap_id, 'Snap.id')
+
+ archive_id = Int(name='archive', allow_none=False)
+ archive = Reference(archive_id, 'Archive.id')
+
+ distro_arch_series_id = Int(name='distro_arch_series', allow_none=False)
+ distro_arch_series = Reference(
+ distro_arch_series_id, 'DistroArchSeries.id')
+
+ pocket = DBEnum(enum=PackagePublishingPocket, allow_none=False)
+
+ processor_id = Int(name='processor', allow_none=False)
+ processor = Reference(processor_id, 'Processor.id')
+ virtualized = Bool(name='virtualized')
+
+ date_created = DateTime(
+ name='date_created', tzinfo=pytz.UTC, allow_none=False)
+ date_started = DateTime(name='date_started', tzinfo=pytz.UTC)
+ date_finished = DateTime(name='date_finished', tzinfo=pytz.UTC)
+ date_first_dispatched = DateTime(
+ name='date_first_dispatched', tzinfo=pytz.UTC)
+
+ builder_id = Int(name='builder')
+ builder = Reference(builder_id, 'Builder.id')
+
+ status = DBEnum(name='status', enum=BuildStatus, allow_none=False)
+
+ log_id = Int(name='log')
+ log = Reference(log_id, 'LibraryFileAlias.id')
+
+ upload_log_id = Int(name='upload_log')
+ upload_log = Reference(upload_log_id, 'LibraryFileAlias.id')
+
+ dependencies = Unicode(name='dependencies')
+
+ failure_count = Int(name='failure_count', allow_none=False)
+
+ def __init__(self, build_farm_job, requester, snap, archive,
+ distro_arch_series, pocket, processor, virtualized,
+ date_created):
+ """Construct a `SnapBuild`."""
+ if not getFeatureFlag(SNAP_FEATURE_FLAG):
+ raise SnapFeatureDisabled
+ super(SnapBuild, self).__init__()
+ self.build_farm_job = build_farm_job
+ self.requester = requester
+ self.snap = snap
+ self.archive = archive
+ self.distro_arch_series = distro_arch_series
+ self.pocket = pocket
+ self.processor = processor
+ self.virtualized = virtualized
+ self.date_created = date_created
+ self.status = BuildStatus.NEEDSBUILD
+
+ @property
+ def is_private(self):
+ """See `IBuildFarmJob`."""
+ return self.snap.owner.private or self.archive.private
+
+ @property
+ def title(self):
+ das = self.distro_arch_series
+ name = self.snap.name
+ return "%s build of %s snap package in %s %s" % (
+ das.architecturetag, name, das.distroseries.distribution.name,
+ das.distroseries.getSuite(self.pocket))
+
+ @property
+ def distribution(self):
+ """See `IPackageBuild`."""
+ return self.distro_arch_series.distroseries.distribution
+
+ @property
+ def distro_series(self):
+ """See `IPackageBuild`."""
+ return self.distro_arch_series.distroseries
+
+ @property
+ def current_component(self):
+ component = self.archive.default_component
+ if component is not None:
+ return component
+ else:
+ # XXX cjwatson 2015-07-17: Hardcode to universe for the time being.
+ return getUtility(IComponentSet)["universe"]
+
+ @property
+ def score(self):
+ """See `ISnapBuild`."""
+ if self.buildqueue_record is None:
+ return None
+ else:
+ return self.buildqueue_record.lastscore
+
+ @property
+ def can_be_rescored(self):
+ """See `ISnapBuild`."""
+ return (
+ self.buildqueue_record is not None and
+ self.status is BuildStatus.NEEDSBUILD)
+
+ @property
+ def can_be_cancelled(self):
+ """See `ISnapBuild`."""
+ if not self.buildqueue_record:
+ return False
+
+ cancellable_statuses = [
+ BuildStatus.BUILDING,
+ BuildStatus.NEEDSBUILD,
+ ]
+ return self.status in cancellable_statuses
+
+ def rescore(self, score):
+ """See `ISnapBuild`."""
+ assert self.can_be_rescored, "Build %s cannot be rescored" % self.id
+ self.buildqueue_record.manualScore(score)
+
+ def cancel(self):
+ """See `ISnapBuild`."""
+ if not self.can_be_cancelled:
+ return
+ # BuildQueue.cancel() will decide whether to go straight to
+ # CANCELLED, or go through CANCELLING to let buildd-manager clean up
+ # the slave.
+ self.buildqueue_record.cancel()
+
+ def calculateScore(self):
+ return 2505 + self.archive.relative_build_score
+
+ def getMedianBuildDuration(self):
+ """Return the median duration of our successful builds."""
+ store = IStore(self)
+ result = store.find(
+ (SnapBuild.date_started, SnapBuild.date_finished),
+ SnapBuild.snap == self.snap_id,
+ SnapBuild.distro_arch_series == self.distro_arch_series_id,
+ SnapBuild.status == BuildStatus.FULLYBUILT)
+ result.order_by(Desc(SnapBuild.date_finished))
+ durations = [row[1] - row[0] for row in result[:9]]
+ if len(durations) == 0:
+ return None
+ durations.sort()
+ return durations[len(durations) // 2]
+
+ def estimateDuration(self):
+ """See `IBuildFarmJob`."""
+ median = self.getMedianBuildDuration()
+ if median is not None:
+ return median
+ return timedelta(minutes=30)
+
+ def getFiles(self):
+ """See `ISnapBuild`."""
+ result = Store.of(self).find(
+ (SnapFile, LibraryFileAlias, LibraryFileContent),
+ SnapFile.snapbuild == self.id,
+ LibraryFileAlias.id == SnapFile.libraryfile_id,
+ LibraryFileContent.id == LibraryFileAlias.contentID)
+ return result.order_by([LibraryFileAlias.filename, SnapFile.id])
+
+ def getFileByName(self, filename):
+ """See `ISnapBuild`."""
+ if filename.endswith(".txt.gz"):
+ file_object = self.log
+ elif filename.endswith("_log.txt"):
+ file_object = self.upload_log
+ else:
+ file_object = Store.of(self).find(
+ LibraryFileAlias,
+ SnapFile.snapbuild == self.id,
+ LibraryFileAlias.id == SnapFile.libraryfile_id,
+ LibraryFileAlias.filename == filename).one()
+
+ if file_object is not None and file_object.filename == filename:
+ return file_object
+
+ raise NotFoundError(filename)
+
+ def addFile(self, lfa):
+ """See `ISnapBuild`."""
+ snapfile = SnapFile(snapbuild=self, libraryfile=lfa)
+ IMasterStore(SnapFile).add(snapfile)
+ return snapfile
+
+ def verifySuccessfulUpload(self):
+ """See `IPackageBuild`."""
+ return not self.getFiles().is_empty()
+
+ def lfaUrl(self, lfa):
+ """Return the URL for a LibraryFileAlias in this context."""
+ if lfa is None:
+ return None
+ return ProxiedLibraryFileAlias(lfa, self).http_url
+
+ @property
+ def log_url(self):
+ """See `IBuildFarmJob`."""
+ return self.lfaUrl(self.log)
+
+ @property
+ def upload_log_url(self):
+ """See `IPackageBuild`."""
+ return self.lfaUrl(self.upload_log)
+
+ def getFileUrls(self):
+ return [self.lfaUrl(lfa) for _, lfa, _ in self.getFiles()]
+
+
+@implementer(ISnapBuildSet)
+class SnapBuildSet(SpecificBuildFarmJobSourceMixin):
+
+ def new(self, requester, snap, archive, distro_arch_series, pocket,
+ date_created=DEFAULT):
+ """See `ISnapBuildSet`."""
+ store = IMasterStore(SnapBuild)
+ build_farm_job = getUtility(IBuildFarmJobSource).new(
+ SnapBuild.job_type, BuildStatus.NEEDSBUILD, date_created, None,
+ archive)
+ snapbuild = SnapBuild(
+ build_farm_job, requester, snap, archive, distro_arch_series,
+ pocket, distro_arch_series.processor,
+ not distro_arch_series.processor.supports_nonvirtualized
+ or snap.require_virtualized or archive.require_virtualized,
+ date_created)
+ store.add(snapbuild)
+ return snapbuild
+
+ def getByID(self, build_id):
+ """See `ISpecificBuildFarmJobSource`."""
+ store = IMasterStore(SnapBuild)
+ return store.get(SnapBuild, build_id)
+
+ def getByBuildFarmJob(self, build_farm_job):
+ """See `ISpecificBuildFarmJobSource`."""
+ return Store.of(build_farm_job).find(
+ SnapBuild, build_farm_job_id=build_farm_job.id).one()
+
+ def preloadBuildsData(self, builds):
+ # Circular import.
+ from lp.snappy.model.snap import Snap
+ load_related(Person, builds, ["requester_id"])
+ load_related(LibraryFileAlias, builds, ["log_id"])
+ archives = load_related(Archive, builds, ["archive_id"])
+ load_related(Person, archives, ["ownerID"])
+ load_related(Snap, builds, ["snap_id"])
+
+ def getByBuildFarmJobs(self, build_farm_jobs):
+ """See `ISpecificBuildFarmJobSource`."""
+ if len(build_farm_jobs) == 0:
+ return EmptyResultSet()
+ rows = Store.of(build_farm_jobs[0]).find(
+ SnapBuild, SnapBuild.build_farm_job_id.is_in(
+ bfj.id for bfj in build_farm_jobs))
+ return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)
=== added file 'lib/lp/snappy/tests/test_snapbuild.py'
--- lib/lp/snappy/tests/test_snapbuild.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_snapbuild.py 2015-07-23 16:10:25 +0000
@@ -0,0 +1,226 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test snap package build features."""
+
+__metaclass__ = type
+
+from datetime import timedelta
+
+from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
+
+from lp.app.errors import NotFoundError
+from lp.buildmaster.enums import BuildStatus
+from lp.buildmaster.interfaces.buildqueue import IBuildQueue
+from lp.buildmaster.interfaces.packagebuild import IPackageBuild
+from lp.registry.enums import PersonVisibility
+from lp.services.features.testing import FeatureFixture
+from lp.snappy.interfaces.snap import (
+ SNAP_FEATURE_FLAG,
+ SnapFeatureDisabled,
+ )
+from lp.snappy.interfaces.snapbuild import (
+ ISnapBuild,
+ ISnapBuildSet,
+ )
+from lp.soyuz.enums import ArchivePurpose
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import LaunchpadZopelessLayer
+
+
+class TestSnapBuildFeatureFlag(TestCaseWithFactory):
+
+ layer = LaunchpadZopelessLayer
+
+ def test_feature_flag_disabled(self):
+ # Without a feature flag, we will not create new SnapBuilds.
+ class MockSnap:
+ require_virtualized = False
+
+ self.assertRaises(
+ SnapFeatureDisabled, getUtility(ISnapBuildSet).new,
+ None, MockSnap(), self.factory.makeArchive(),
+ self.factory.makeDistroArchSeries(), None)
+
+
+class TestSnapBuild(TestCaseWithFactory):
+
+ layer = LaunchpadZopelessLayer
+
+ def setUp(self):
+ super(TestSnapBuild, self).setUp()
+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+ self.build = self.factory.makeSnapBuild()
+
+ def test_implements_interfaces(self):
+ # SnapBuild implements IPackageBuild and ISnapBuild.
+ self.assertProvides(self.build, IPackageBuild)
+ self.assertProvides(self.build, ISnapBuild)
+
+ def test_queueBuild(self):
+ # SnapBuild can create the queue entry for itself.
+ bq = self.build.queueBuild()
+ self.assertProvides(bq, IBuildQueue)
+ self.assertEqual(
+ self.build.build_farm_job, removeSecurityProxy(bq)._build_farm_job)
+ self.assertEqual(self.build, bq.specific_build)
+ self.assertEqual(self.build.virtualized, bq.virtualized)
+ self.assertIsNotNone(bq.processor)
+ self.assertEqual(bq, self.build.buildqueue_record)
+
+ def test_current_component_primary(self):
+ # SnapBuilds for primary archives always build in universe for the
+ # time being.
+ self.assertEqual(ArchivePurpose.PRIMARY, self.build.archive.purpose)
+ self.assertEqual("universe", self.build.current_component.name)
+
+ def test_current_component_ppa(self):
+ # PPAs only have indices for main, so SnapBuilds for PPAs always
+ # build in main.
+ build = self.factory.makeSnapBuild(archive=self.factory.makeArchive())
+ self.assertEqual("main", build.current_component.name)
+
+ def test_is_private(self):
+ # A SnapBuild is private iff its Snap and archive are.
+ self.assertFalse(self.build.is_private)
+ private_team = self.factory.makeTeam(
+ visibility=PersonVisibility.PRIVATE)
+ with person_logged_in(private_team.teamowner):
+ build = self.factory.makeSnapBuild(
+ requester=private_team.teamowner, owner=private_team)
+ self.assertTrue(build.is_private)
+ private_archive = self.factory.makeArchive(private=True)
+ with person_logged_in(private_archive.owner):
+ build = self.factory.makeSnapBuild(archive=private_archive)
+ self.assertTrue(build.is_private)
+
+ def test_can_be_cancelled(self):
+ # For all states that can be cancelled, can_be_cancelled returns True.
+ ok_cases = [
+ BuildStatus.BUILDING,
+ BuildStatus.NEEDSBUILD,
+ ]
+ for status in BuildStatus:
+ if status in ok_cases:
+ self.assertTrue(self.build.can_be_cancelled)
+ else:
+ self.assertFalse(self.build.can_be_cancelled)
+
+ def test_cancel_not_in_progress(self):
+ # The cancel() method for a pending build leaves it in the CANCELLED
+ # state.
+ self.build.queueBuild()
+ self.build.cancel()
+ self.assertEqual(BuildStatus.CANCELLED, self.build.status)
+ self.assertIsNone(self.build.buildqueue_record)
+
+ def test_cancel_in_progress(self):
+ # The cancel() method for a building build leaves it in the
+ # CANCELLING state.
+ bq = self.build.queueBuild()
+ bq.markAsBuilding(self.factory.makeBuilder())
+ self.build.cancel()
+ self.assertEqual(BuildStatus.CANCELLING, self.build.status)
+ self.assertEqual(bq, self.build.buildqueue_record)
+
+ def test_estimateDuration(self):
+ # Without previous builds, the default time estimate is 30m.
+ self.assertEqual(1800, self.build.estimateDuration().seconds)
+
+ def test_estimateDuration_with_history(self):
+ # Previous successful builds of the same snap package are used for
+ # estimates.
+ self.factory.makeSnapBuild(
+ requester=self.build.requester, snap=self.build.snap,
+ distroarchseries=self.build.distro_arch_series,
+ status=BuildStatus.FULLYBUILT, duration=timedelta(seconds=335))
+ for i in range(3):
+ self.factory.makeSnapBuild(
+ requester=self.build.requester, snap=self.build.snap,
+ distroarchseries=self.build.distro_arch_series,
+ status=BuildStatus.FAILEDTOBUILD,
+ duration=timedelta(seconds=20))
+ self.assertEqual(335, self.build.estimateDuration().seconds)
+
+ def test_build_cookie(self):
+ build = self.factory.makeSnapBuild()
+ self.assertEqual('SNAPBUILD-%d' % build.id, build.build_cookie)
+
+ def test_getFileByName_logs(self):
+ # getFileByName returns the logs when requested by name.
+ self.build.setLog(
+ self.factory.makeLibraryFileAlias(filename="buildlog.txt.gz"))
+ self.assertEqual(
+ self.build.log, self.build.getFileByName("buildlog.txt.gz"))
+ self.assertRaises(NotFoundError, self.build.getFileByName, "foo")
+ self.build.storeUploadLog("uploaded")
+ self.assertEqual(
+ self.build.upload_log,
+ self.build.getFileByName(self.build.upload_log.filename))
+
+ def test_getFileByName_uploaded_files(self):
+ # getFileByName returns uploaded files when requested by name.
+ filenames = ("ubuntu.squashfs", "ubuntu.manifest")
+ lfas = []
+ for filename in filenames:
+ lfa = self.factory.makeLibraryFileAlias(filename=filename)
+ lfas.append(lfa)
+ self.build.addFile(lfa)
+ self.assertContentEqual(
+ lfas, [row[1] for row in self.build.getFiles()])
+ for filename, lfa in zip(filenames, lfas):
+ self.assertEqual(lfa, self.build.getFileByName(filename))
+ self.assertRaises(NotFoundError, self.build.getFileByName, "missing")
+
+ def test_verifySuccessfulUpload(self):
+ self.assertFalse(self.build.verifySuccessfulUpload())
+ self.factory.makeSnapFile(snapbuild=self.build)
+ self.assertTrue(self.build.verifySuccessfulUpload())
+
+ def addFakeBuildLog(self, build):
+ build.setLog(self.factory.makeLibraryFileAlias("mybuildlog.txt"))
+
+ def test_log_url(self):
+ # The log URL for a snap package build will use the archive context.
+ self.addFakeBuildLog(self.build)
+ self.assertEqual(
+ "http://launchpad.dev/~%s/+snap/%s/+build/%d/+files/"
+ "mybuildlog.txt" % (
+ self.build.snap.owner.name, self.build.snap.name,
+ self.build.id),
+ self.build.log_url)
+
+
+class TestSnapBuildSet(TestCaseWithFactory):
+
+ layer = LaunchpadZopelessLayer
+
+ def setUp(self):
+ super(TestSnapBuildSet, self).setUp()
+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+
+ def test_getByBuildFarmJob_works(self):
+ build = self.factory.makeSnapBuild()
+ self.assertEqual(
+ build,
+ getUtility(ISnapBuildSet).getByBuildFarmJob(build.build_farm_job))
+
+ def test_getByBuildFarmJob_returns_None_when_missing(self):
+ bpb = self.factory.makeBinaryPackageBuild()
+ self.assertIsNone(
+ getUtility(ISnapBuildSet).getByBuildFarmJob(bpb.build_farm_job))
+
+ def test_getByBuildFarmJobs_works(self):
+ builds = [self.factory.makeSnapBuild() for i in range(10)]
+ self.assertContentEqual(
+ builds,
+ getUtility(ISnapBuildSet).getByBuildFarmJobs(
+ [build.build_farm_job for build in builds]))
+
+ def test_getByBuildFarmJobs_works_empty(self):
+ self.assertContentEqual(
+ [], getUtility(ISnapBuildSet).getByBuildFarmJobs([]))
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2015-07-23 16:10:25 +0000
+++ lib/lp/testing/factory.py 2015-07-23 16:10:25 +0000
@@ -265,6 +265,8 @@
from lp.services.worlddata.interfaces.country import ICountrySet
from lp.services.worlddata.interfaces.language import ILanguageSet
from lp.snappy.interfaces.snap import ISnapSet
+from lp.snappy.interfaces.snapbuild import ISnapBuildSet
+from lp.snappy.model.snapbuild import SnapFile
from lp.soyuz.adapters.overrides import SourceOverride
from lp.soyuz.adapters.packagelocation import PackageLocation
from lp.soyuz.enums import (
@@ -4559,6 +4561,59 @@
IStore(snap).flush()
return snap
+ def makeSnapBuild(self, requester=None, registrant=None, snap=None,
+ archive=None, distroarchseries=None, pocket=None,
+ date_created=DEFAULT, status=BuildStatus.NEEDSBUILD,
+ builder=None, duration=None, **kwargs):
+ """Make a new SnapBuild."""
+ if requester is None:
+ requester = self.makePerson()
+ if snap is None:
+ if "distroseries" in kwargs:
+ distroseries = kwargs["distroseries"]
+ del kwargs["distroseries"]
+ elif distroarchseries is not None:
+ distroseries = distroarchseries.distroseries
+ elif archive is not None:
+ distroseries = self.makeDistroSeries(
+ distribution=archive.distribution)
+ else:
+ distroseries = None
+ if registrant is None:
+ registrant = requester
+ snap = self.makeSnap(
+ registrant=registrant, distroseries=distroseries, **kwargs)
+ if archive is None:
+ archive = snap.distro_series.main_archive
+ if distroarchseries is None:
+ distroarchseries = self.makeDistroArchSeries(
+ distroseries=snap.distro_series)
+ if pocket is None:
+ pocket = PackagePublishingPocket.RELEASE
+ snapbuild = getUtility(ISnapBuildSet).new(
+ requester, snap, archive, distroarchseries, pocket,
+ date_created=date_created)
+ if duration is not None:
+ removeSecurityProxy(snapbuild).updateStatus(
+ BuildStatus.BUILDING, builder=builder,
+ date_started=snapbuild.date_created)
+ removeSecurityProxy(snapbuild).updateStatus(
+ status, builder=builder,
+ date_finished=snapbuild.date_started + duration)
+ else:
+ removeSecurityProxy(snapbuild).updateStatus(
+ status, builder=builder)
+ IStore(snapbuild).flush()
+ return snapbuild
+
+ def makeSnapFile(self, snapbuild=None, libraryfile=None):
+ if snapbuild is None:
+ snapbuild = self.makeSnapBuild()
+ if libraryfile is None:
+ libraryfile = self.makeLibraryFileAlias()
+ return ProxyFactory(
+ SnapFile(snapbuild=snapbuild, libraryfile=libraryfile))
+
# Some factory methods return simple Python types. We don't add
# security wrappers for them, as well as for objects created by
Follow ups