launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19059
[Merge] lp:~cjwatson/launchpad/snap-basic-model into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-basic-model into lp:launchpad with lp:~cjwatson/launchpad/snap-personmerge-whitelist as a prerequisite.
Commit message:
Add basic model for snap packages.
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/snap-basic-model/+merge/265671
Add basic model for snap packages.
For the sake of the branch not being enormous, this isn't very useful yet, as SnapBuild and SnapFile aren't modelled.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-basic-model into lp:launchpad.
=== modified file 'lib/lp/app/browser/configure.zcml'
--- lib/lp/app/browser/configure.zcml 2015-04-21 10:49:24 +0000
+++ lib/lp/app/browser/configure.zcml 2015-07-23 14:38:45 +0000
@@ -841,6 +841,12 @@
name="fmt"
/>
<adapter
+ for="lp.snappy.interfaces.snap.ISnap"
+ provides="zope.traversing.interfaces.IPathAdapter"
+ factory="lp.app.browser.tales.SnapFormatterAPI"
+ name="fmt"
+ />
+ <adapter
for="lp.blueprints.interfaces.specification.ISpecification"
provides="zope.traversing.interfaces.IPathAdapter"
factory="lp.app.browser.tales.SpecificationFormatterAPI"
=== modified file 'lib/lp/app/browser/tales.py'
--- lib/lp/app/browser/tales.py 2015-07-09 12:18:51 +0000
+++ lib/lp/app/browser/tales.py 2015-07-23 14:38:45 +0000
@@ -1858,6 +1858,17 @@
'owner': self._context.owner.displayname}
+class SnapFormatterAPI(CustomizableFormatter):
+ """Adapter providing fmt support for ISnap objects."""
+
+ _link_summary_template = _(
+ 'Snap %(name)s for %(owner)s')
+
+ def _link_summary_values(self):
+ return {'name': self._context.name,
+ 'owner': self._context.owner.displayname}
+
+
class SpecificationFormatterAPI(CustomizableFormatter):
"""Adapter providing fmt support for Specification objects"""
=== modified file 'lib/lp/configure.zcml'
--- lib/lp/configure.zcml 2013-06-03 07:26:52 +0000
+++ lib/lp/configure.zcml 2015-07-23 14:38:45 +0000
@@ -30,6 +30,7 @@
<include package="lp.code" />
<include package="lp.coop.answersbugs" />
<include package="lp.hardwaredb" />
+ <include package="lp.snappy" />
<include package="lp.soyuz" />
<include package="lp.translations" />
<include package="lp.testing" />
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2015-07-21 09:04:01 +0000
+++ lib/lp/registry/browser/person.py 2015-07-23 14:38:45 +0000
@@ -274,6 +274,7 @@
from lp.services.webapp.publisher import LaunchpadView
from lp.services.worlddata.interfaces.country import ICountry
from lp.services.worlddata.interfaces.language import ILanguageSet
+from lp.snappy.interfaces.snap import ISnapSet
from lp.soyuz.browser.archivesubscription import (
traverse_archive_subscription_for_subscriber,
)
@@ -619,6 +620,11 @@
return livefs
+ @stepthrough('+snap')
+ def traverse_snap(self, name):
+ """Traverse to this person's snap packages."""
+ return getUtility(ISnapSet).getByName(self.context, name)
+
class PersonSetNavigation(Navigation):
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2015-07-12 23:48:01 +0000
+++ lib/lp/security.py 2015-07-23 14:38:45 +0000
@@ -29,7 +29,6 @@
from lp.answers.interfaces.questionsperson import IQuestionsPerson
from lp.answers.interfaces.questiontarget import IQuestionTarget
from lp.app.interfaces.security import IAuthorization
-from lp.app.interfaces.services import IService
from lp.app.security import (
AnonymousAuthorization,
AuthorizationBase,
@@ -192,6 +191,7 @@
ILanguage,
ILanguageSet,
)
+from lp.snappy.interfaces.snap import ISnap
from lp.soyuz.interfaces.archive import IArchive
from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken
from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
@@ -3080,3 +3080,39 @@
def __init__(self, obj):
super(ViewWebhookDeliveryJob, self).__init__(
obj, obj.webhook, 'launchpad.View')
+
+
+class ViewSnap(DelegatedAuthorization):
+ permission = 'launchpad.View'
+ usedfor = ISnap
+
+ def __init__(self, obj):
+ super(ViewSnap, self).__init__(obj, obj.owner, 'launchpad.View')
+
+
+class EditSnap(AuthorizationBase):
+ permission = 'launchpad.Edit'
+ usedfor = ISnap
+
+ def checkAuthenticated(self, user):
+ return (
+ user.isOwner(self.obj) or
+ user.in_commercial_admin or user.in_admin)
+
+
+class AdminSnap(AuthorizationBase):
+ """Restrict changing build settings on snap packages.
+
+ The security of the non-virtualised build farm depends on these
+ settings, so they can only be changed by commercial admins, or by "PPA"
+ self admins on snap packages that they can already edit.
+ """
+ permission = 'launchpad.Admin'
+ usedfor = ISnap
+
+ def checkAuthenticated(self, user):
+ if user.in_commercial_admin or user.in_admin:
+ return True
+ return (
+ user.in_ppa_self_admins
+ and EditSnap(self.obj).checkAuthenticated(user))
=== added directory 'lib/lp/snappy'
=== added file 'lib/lp/snappy/__init__.py'
=== added directory 'lib/lp/snappy/browser'
=== added file 'lib/lp/snappy/browser/__init__.py'
=== added file 'lib/lp/snappy/browser/configure.zcml'
--- lib/lp/snappy/browser/configure.zcml 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/browser/configure.zcml 2015-07-23 14:38:45 +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).
+-->
+
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ xmlns:i18n="http://namespaces.zope.org/i18n"
+ xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
+ i18n_domain="launchpad">
+ <facet facet="overview">
+ <browser:url
+ for="lp.snappy.interfaces.snap.ISnap"
+ path_expression="string:+snap/${name}"
+ attribute_to_parent="owner" />
+ </facet>
+</configure>
=== added file 'lib/lp/snappy/configure.zcml'
--- lib/lp/snappy/configure.zcml 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/configure.zcml 2015-07-23 14:38:45 +0000
@@ -0,0 +1,42 @@
+<!-- Copyright 2015 Canonical Ltd. This software is licensed under the
+ GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ xmlns:i18n="http://namespaces.zope.org/i18n"
+ xmlns:lp="http://namespaces.canonical.com/lp"
+ xmlns:webservice="http://namespaces.canonical.com/webservice"
+ xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
+ i18n_domain="launchpad">
+
+ <include package=".browser" />
+
+ <!-- Snap -->
+ <class class="lp.snappy.model.snap.Snap">
+ <require
+ permission="launchpad.View"
+ interface="lp.snappy.interfaces.snap.ISnapView
+ lp.snappy.interfaces.snap.ISnapEditableAttributes
+ lp.snappy.interfaces.snap.ISnapAdminAttributes" />
+ <require
+ permission="launchpad.Edit"
+ interface="lp.snappy.interfaces.snap.ISnapEdit"
+ set_schema="lp.snappy.interfaces.snap.ISnapEditableAttributes" />
+ <require
+ permission="launchpad.Admin"
+ set_schema="lp.snappy.interfaces.snap.ISnapAdminAttributes" />
+ </class>
+ <subscriber
+ for="lp.snappy.interfaces.snap.ISnap zope.lifecycleevent.interfaces.IObjectModifiedEvent"
+ handler="lp.snappy.model.snap.snap_modified" />
+
+ <!-- SnapSet -->
+ <securedutility
+ class="lp.snappy.model.snap.SnapSet"
+ provides="lp.snappy.interfaces.snap.ISnapSet">
+ <allow interface="lp.snappy.interfaces.snap.ISnapSet" />
+ </securedutility>
+
+</configure>
=== added directory 'lib/lp/snappy/interfaces'
=== added file 'lib/lp/snappy/interfaces/__init__.py'
=== added file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/interfaces/snap.py 2015-07-23 14:38:45 +0000
@@ -0,0 +1,235 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Snap package interfaces."""
+
+__metaclass__ = type
+
+__all__ = [
+ 'CannotDeleteSnap',
+ 'DuplicateSnapName',
+ 'ISnap',
+ 'ISnapSet',
+ 'ISnapView',
+ 'SNAP_FEATURE_FLAG',
+ 'SnapBuildAlreadyPending',
+ 'SnapBuildArchiveOwnerMismatch',
+ 'SnapFeatureDisabled',
+ 'SnapNotOwner',
+ 'NoSuchSnap',
+ ]
+
+from lazr.restful.fields import (
+ Reference,
+ ReferenceChoice,
+ )
+from zope.interface import (
+ Attribute,
+ Interface,
+ )
+from zope.schema import (
+ Bool,
+ Datetime,
+ Int,
+ Text,
+ TextLine,
+ )
+from zope.security.interfaces import (
+ Forbidden,
+ Unauthorized,
+ )
+
+from lp import _
+from lp.app.errors import NameLookupFailed
+from lp.app.validators.name import name_validator
+from lp.code.interfaces.branch import IBranch
+from lp.code.interfaces.gitrepository import IGitRepository
+from lp.registry.interfaces.distroseries import IDistroSeries
+from lp.registry.interfaces.role import IHasOwner
+from lp.services.fields import (
+ PersonChoice,
+ PublicPersonChoice,
+ )
+
+
+SNAP_FEATURE_FLAG = u"snap.allow_new"
+
+
+class SnapBuildAlreadyPending(Exception):
+ """A build was requested when an identical build was already pending."""
+
+ def __init__(self):
+ super(SnapBuildAlreadyPending, self).__init__(
+ "An identical build of this snap package is already pending.")
+
+
+class SnapBuildArchiveOwnerMismatch(Forbidden):
+ """Builds against private archives require that owners match.
+
+ The snap package owner must have write permission on the archive, so
+ that a malicious snap package build can't steal any secrets that its
+ owner didn't already have access to. Furthermore, we want to make sure
+ that future changes to the team owning the snap package don't grant it
+ retrospective access to information about a private archive. For now,
+ the simplest way to do this is to require that the owners match exactly.
+ """
+
+ def __init__(self):
+ super(SnapBuildArchiveOwnerMismatch, self).__init__(
+ "Snap package builds against private archives are only allowed "
+ "if the snap package owner and the archive owner are equal.")
+
+
+class SnapFeatureDisabled(Unauthorized):
+ """Only certain users can create new snap-related objects."""
+
+ def __init__(self):
+ super(SnapFeatureDisabled, self).__init__(
+ "You do not have permission to create new snaps or new snap "
+ "builds.")
+
+
+class DuplicateSnapName(Exception):
+ """Raised for snap packages with duplicate name/owner."""
+
+ def __init__(self):
+ super(DuplicateSnapName, self).__init__(
+ "There is already a snap package with the same name and owner.")
+
+
+class SnapNotOwner(Unauthorized):
+ """The registrant/requester is not the owner or a member of its team."""
+
+
+class NoSuchSnap(NameLookupFailed):
+ """The requested snap package does not exist."""
+ _message_prefix = "No such snap package with this owner"
+
+
+class CannotDeleteSnap(Exception):
+ """This snap package cannot be deleted."""
+
+
+class ISnapView(Interface):
+ """`ISnap` attributes that require launchpad.View permission."""
+
+ id = Int(title=_("ID"), required=True, readonly=True)
+
+ date_created = Datetime(
+ title=_("Date created"), required=True, readonly=True)
+
+ registrant = PublicPersonChoice(
+ title=_("Registrant"), required=True, readonly=True,
+ vocabulary="ValidPersonOrTeam",
+ description=_("The person who registered this snap package."))
+
+ def requestBuild(requester, archive, distro_arch_series, pocket):
+ """Request that the snap package be built.
+
+ :param requester: The person requesting the build.
+ :param archive: The IArchive to associate the build with.
+ :param distro_arch_series: The architecture to build for.
+ :param pocket: The pocket that should be targeted.
+ :return: `ISnapBuild`.
+ """
+
+ builds = Attribute("All builds of this snap package.")
+
+ completed_builds = Attribute("Completed builds of this snap package.")
+
+ pending_builds = Attribute("Pending builds of this snap package.")
+
+
+class ISnapEdit(Interface):
+ """`ISnap` methods that require launchpad.Edit permission."""
+
+ def destroySelf():
+ """Delete this snap package, provided that it has no builds."""
+
+
+class ISnapEditableAttributes(IHasOwner):
+ """`ISnap` attributes that can be edited.
+
+ These attributes need launchpad.View to see, and launchpad.Edit to change.
+ """
+ date_last_modified = Datetime(
+ title=_("Date last modified"), required=True, readonly=True)
+
+ owner = PersonChoice(
+ title=_("Owner"), required=True, readonly=False,
+ vocabulary="AllUserTeamsParticipationPlusSelf",
+ description=_("The owner of this snap package."))
+
+ distro_series = Reference(
+ IDistroSeries, title=_("Distro Series"), required=True, readonly=False,
+ description=_(
+ "The series for which the snap package should be built."))
+
+ name = TextLine(
+ title=_("Name"), required=True, readonly=False,
+ constraint=name_validator,
+ description=_("The name of the snap package."))
+
+ description = Text(
+ title=_("Description"), required=False, readonly=False,
+ description=_("A description of the snap package."))
+
+ branch = ReferenceChoice(
+ title=_("Bazaar branch"), schema=IBranch, vocabulary="Branch",
+ required=False, readonly=False,
+ description=_(
+ "A Bazaar branch containing a snapcraft.yaml recipe at the top "
+ "level."))
+
+ git_repository = ReferenceChoice(
+ title=_("Git repository"),
+ schema=IGitRepository, vocabulary="GitRepository",
+ required=False, readonly=False,
+ description=_(
+ "A Git repository with a branch containing a snapcraft.yaml "
+ "recipe at the top level."))
+
+ git_path = TextLine(
+ title=_("Git branch path"), required=False, readonly=False,
+ description=_(
+ "The path of the Git branch containing a snapcraft.yaml recipe at "
+ "the top level."))
+
+
+class ISnapAdminAttributes(Interface):
+ """`ISnap` attributes that can be edited by admins.
+
+ These attributes need launchpad.View to see, and launchpad.Admin to change.
+ """
+ require_virtualized = Bool(
+ title=_("Require virtualized builders"), required=True, readonly=False,
+ description=_("Only build this snap package on virtual builders."))
+
+
+class ISnap(
+ ISnapView, ISnapEdit, ISnapEditableAttributes, ISnapAdminAttributes):
+ """A buildable snap package."""
+
+
+class ISnapSet(Interface):
+ """A utility to create and access snap packages."""
+
+ def new(registrant, owner, distro_series, name, description=None,
+ branch=None, git_repository=None, git_path=None,
+ require_virtualized=True, date_created=None):
+ """Create an `ISnap`."""
+
+ def exists(owner, name):
+ """Check to see if a matching snap exists."""
+
+ def getByName(owner, name):
+ """Return the appropriate `ISnap` for the given objects."""
+
+ def getByPerson(owner):
+ """Return all snap packages with the given `owner`."""
+
+ def empty_list():
+ """Return an empty collection of snap packages.
+
+ This only exists to keep lazr.restful happy.
+ """
=== added directory 'lib/lp/snappy/model'
=== added file 'lib/lp/snappy/model/__init__.py'
=== added file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/model/snap.py 2015-07-23 14:38:45 +0000
@@ -0,0 +1,189 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+__all__ = [
+ 'Snap',
+ ]
+
+import pytz
+from storm.exceptions import IntegrityError
+from storm.locals import (
+ Bool,
+ DateTime,
+ Int,
+ Reference,
+ Storm,
+ Unicode,
+ )
+from zope.interface import implementer
+
+from lp.registry.interfaces.role import IHasOwner
+from lp.services.database.constants import (
+ DEFAULT,
+ UTC_NOW,
+ )
+from lp.services.database.interfaces import (
+ IMasterStore,
+ IStore,
+ )
+from lp.services.features import getFeatureFlag
+from lp.snappy.interfaces.snap import (
+ DuplicateSnapName,
+ ISnap,
+ ISnapSet,
+ SNAP_FEATURE_FLAG,
+ SnapFeatureDisabled,
+ SnapNotOwner,
+ NoSuchSnap,
+ )
+
+
+def snap_modified(snap, event):
+ """Update the date_last_modified property when a Snap is modified.
+
+ This method is registered as a subscriber to `IObjectModifiedEvent`
+ events on snap packages.
+ """
+ snap.date_last_modified = UTC_NOW
+
+
+@implementer(ISnap, IHasOwner)
+class Snap(Storm):
+ """See `ISnap`."""
+
+ __storm_table__ = 'Snap'
+
+ id = Int(primary=True)
+
+ date_created = DateTime(
+ name='date_created', tzinfo=pytz.UTC, allow_none=False)
+ date_last_modified = DateTime(
+ name='date_last_modified', tzinfo=pytz.UTC, allow_none=False)
+
+ registrant_id = Int(name='registrant', allow_none=False)
+ registrant = Reference(registrant_id, 'Person.id')
+
+ owner_id = Int(name='owner', allow_none=False)
+ owner = Reference(owner_id, 'Person.id')
+
+ distro_series_id = Int(name='distro_series', allow_none=False)
+ distro_series = Reference(distro_series_id, 'DistroSeries.id')
+
+ name = Unicode(name='name', allow_none=False)
+
+ description = Unicode(name='description', allow_none=True)
+
+ branch_id = Int(name='branch', allow_none=True)
+ branch = Reference(branch_id, 'Branch.id')
+
+ git_repository_id = Int(name='git_repository', allow_none=True)
+ git_repository = Reference(git_repository_id, 'GitRepository.id')
+
+ git_path = Unicode(name='git_path', allow_none=True)
+
+ require_virtualized = Bool(name='require_virtualized')
+
+ def __init__(self, registrant, owner, distro_series, name,
+ description=None, branch=None, git_repository=None,
+ git_path=None, require_virtualized=True,
+ date_created=DEFAULT):
+ """Construct a `Snap`."""
+ if not getFeatureFlag(SNAP_FEATURE_FLAG):
+ raise SnapFeatureDisabled
+ super(Snap, self).__init__()
+ self.registrant = registrant
+ self.owner = owner
+ self.distro_series = distro_series
+ self.name = name
+ self.description = description
+ self.branch = branch
+ self.git_repository = git_repository
+ self.git_path = git_path
+ self.require_virtualized = require_virtualized
+ self.date_created = date_created
+ self.date_last_modified = date_created
+
+ def requestBuild(self, requester, archive, distro_arch_series, pocket):
+ """See `ISnap`."""
+ raise NotImplementedError
+
+ @property
+ def builds(self):
+ """See `ISnap`."""
+ return []
+
+ @property
+ def _pending_states(self):
+ """All the build states we consider pending (non-final)."""
+ raise NotImplementedError
+
+ @property
+ def completed_builds(self):
+ """See `ISnap`."""
+ return []
+
+ @property
+ def pending_builds(self):
+ """See `ISnap`."""
+ return []
+
+ def destroySelf(self):
+ """See `ISnap`."""
+ raise NotImplementedError
+
+
+@implementer(ISnapSet)
+class SnapSet:
+ """See `ISnapSet`."""
+
+ def new(self, registrant, owner, distro_series, name, description=None,
+ branch=None, git_repository=None, git_path=None,
+ require_virtualized=True, date_created=DEFAULT):
+ """See `ISnapSet`."""
+ if not registrant.inTeam(owner):
+ if owner.is_team:
+ raise SnapNotOwner(
+ "%s is not a member of %s." %
+ (registrant.displayname, owner.displayname))
+ else:
+ raise SnapNotOwner(
+ "%s cannot create snap packages owned by %s." %
+ (registrant.displayname, owner.displayname))
+
+ store = IMasterStore(Snap)
+ snap = Snap(
+ registrant, owner, distro_series, name, description=description,
+ branch=branch, git_repository=git_repository, git_path=git_path,
+ require_virtualized=require_virtualized, date_created=date_created)
+ store.add(snap)
+
+ try:
+ store.flush()
+ except IntegrityError:
+ raise DuplicateSnapName
+
+ return snap
+
+ def _getByName(self, owner, name):
+ return IStore(Snap).find(
+ Snap, Snap.owner == owner, Snap.name == name).one()
+
+ def exists(self, owner, name):
+ """See `ISnapSet`."""
+ return self._getByName(owner, name) is not None
+
+ def getByName(self, owner, name):
+ """See `ISnapSet`."""
+ snap = self._getByName(owner, name)
+ if snap is None:
+ raise NoSuchSnap(name)
+ return snap
+
+ def getByPerson(self, owner):
+ """See `ISnapSet`."""
+ return IStore(Snap).find(Snap, Snap.owner == owner)
+
+ def empty_list(self):
+ """See `ISnapSet`."""
+ return []
=== added directory 'lib/lp/snappy/tests'
=== added file 'lib/lp/snappy/tests/__init__.py'
=== added file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/tests/test_snap.py 2015-07-23 14:38:45 +0000
@@ -0,0 +1,173 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test snap packages."""
+
+__metaclass__ = type
+
+from datetime import datetime
+
+from lazr.lifecycle.event import ObjectModifiedEvent
+import pytz
+from storm.locals import Store
+import transaction
+from zope.component import getUtility
+from zope.event import notify
+from zope.security.proxy import removeSecurityProxy
+
+from lp.buildmaster.enums import (
+ BuildQueueStatus,
+ BuildStatus,
+ )
+from lp.buildmaster.interfaces.buildqueue import IBuildQueue
+from lp.buildmaster.model.buildqueue import BuildQueue
+from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.services.database.constants import UTC_NOW
+from lp.services.features.testing import FeatureFixture
+from lp.snappy.interfaces.snap import (
+ CannotDeleteSnap,
+ ISnap,
+ ISnapSet,
+ SNAP_FEATURE_FLAG,
+ SnapBuildAlreadyPending,
+ SnapFeatureDisabled,
+ )
+from lp.testing import (
+ person_logged_in,
+ TestCaseWithFactory,
+ )
+from lp.testing.layers import (
+ DatabaseFunctionalLayer,
+ LaunchpadZopelessLayer,
+ )
+
+
+class TestSnapFeatureFlag(TestCaseWithFactory):
+
+ layer = LaunchpadZopelessLayer
+
+ def test_feature_flag_disabled(self):
+ # Without a feature flag, we will not create new Snaps.
+ person = self.factory.makePerson()
+ self.assertRaises(
+ SnapFeatureDisabled, getUtility(ISnapSet).new,
+ person, person, None, None, True, None)
+
+
+class TestSnap(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestSnap, self).setUp()
+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+
+ def test_implements_interfaces(self):
+ # Snap implements ISnap.
+ snap = self.factory.makeSnap()
+ with person_logged_in(snap.owner):
+ self.assertProvides(snap, ISnap)
+
+ def test_initial_date_last_modified(self):
+ # The initial value of date_last_modified is date_created.
+ snap = self.factory.makeSnap(
+ date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC))
+ self.assertEqual(snap.date_created, snap.date_last_modified)
+
+ def test_modifiedevent_sets_date_last_modified(self):
+ # When a Snap receives an object modified event, the last modified
+ # date is set to UTC_NOW.
+ snap = self.factory.makeSnap(
+ date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC))
+ notify(ObjectModifiedEvent(
+ removeSecurityProxy(snap), snap, [ISnap["name"]]))
+ self.assertSqlAttributeEqualsDate(snap, "date_last_modified", UTC_NOW)
+
+
+class TestSnapSet(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestSnapSet, self).setUp()
+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"}))
+
+ def test_class_implements_interfaces(self):
+ # The SnapSet class implements ISnapSet.
+ self.assertProvides(getUtility(ISnapSet), ISnapSet)
+
+ def makeSnapComponents(self, branch=None, git_ref=None):
+ """Return a dict of values that can be used to make a Snap.
+
+ Suggested use: provide as kwargs to ISnapSet.new.
+
+ :param branch: An `IBranch`, or None.
+ :param git_ref: An `IGitRef`, or None.
+ """
+ registrant = self.factory.makePerson()
+ components = dict(
+ registrant=registrant,
+ owner=self.factory.makeTeam(owner=registrant),
+ distro_series=self.factory.makeDistroSeries(),
+ name=self.factory.getUniqueString(u"snap-name"))
+ if branch is None and git_ref is None:
+ branch = self.factory.makeAnyBranch()
+ if branch is not None:
+ components["branch"] = branch
+ else:
+ components["git_repository"] = git_ref.repository
+ components["git_path"] = git_ref.path
+ return components
+
+ def test_creation_bzr(self):
+ # The metadata entries supplied when a Snap is created for a Bazaar
+ # branch are present on the new object.
+ branch = self.factory.makeAnyBranch()
+ components = self.makeSnapComponents(branch=branch)
+ snap = getUtility(ISnapSet).new(**components)
+ transaction.commit()
+ self.assertEqual(components["registrant"], snap.registrant)
+ self.assertEqual(components["owner"], snap.owner)
+ self.assertEqual(components["distro_series"], snap.distro_series)
+ self.assertEqual(components["name"], snap.name)
+ self.assertEqual(branch, snap.branch)
+ self.assertIsNone(snap.git_repository)
+ self.assertIsNone(snap.git_path)
+ self.assertTrue(snap.require_virtualized)
+
+ def test_creation_git(self):
+ # The metadata entries supplied when a Snap is created for a Git
+ # branch are present on the new object.
+ [ref] = self.factory.makeGitRefs()
+ components = self.makeSnapComponents(git_ref=ref)
+ snap = getUtility(ISnapSet).new(**components)
+ transaction.commit()
+ self.assertEqual(components["registrant"], snap.registrant)
+ self.assertEqual(components["owner"], snap.owner)
+ self.assertEqual(components["distro_series"], snap.distro_series)
+ self.assertEqual(components["name"], snap.name)
+ self.assertIsNone(snap.branch)
+ self.assertEqual(ref.repository, snap.git_repository)
+ self.assertEqual(ref.path, snap.git_path)
+ self.assertTrue(snap.require_virtualized)
+
+ def test_exists(self):
+ # ISnapSet.exists checks for matching Snaps.
+ snap = self.factory.makeSnap()
+ self.assertTrue(getUtility(ISnapSet).exists(snap.owner, snap.name))
+ self.assertFalse(
+ getUtility(ISnapSet).exists(self.factory.makePerson(), snap.name))
+ self.assertFalse(getUtility(ISnapSet).exists(snap.owner, u"different"))
+
+ def test_getByPerson(self):
+ # ISnapSet.getByPerson returns all Snaps with the given owner.
+ owners = [self.factory.makePerson() for i in range(2)]
+ snaps = []
+ for owner in owners:
+ for i in range(2):
+ snaps.append(self.factory.makeSnap(
+ registrant=owner, owner=owner))
+ self.assertContentEqual(
+ snaps[:2], getUtility(ISnapSet).getByPerson(owners[0]))
+ self.assertContentEqual(
+ snaps[2:], getUtility(ISnapSet).getByPerson(owners[1]))
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2015-07-07 04:02:36 +0000
+++ lib/lp/testing/factory.py 2015-07-23 14:38:45 +0000
@@ -264,6 +264,7 @@
from lp.services.webhooks.interfaces import IWebhookSource
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.soyuz.adapters.overrides import SourceOverride
from lp.soyuz.adapters.packagelocation import PackageLocation
from lp.soyuz.enums import (
@@ -4531,6 +4532,33 @@
target, self.makePerson(), delivery_url, [], True,
self.getUniqueUnicode())
+ def makeSnap(self, registrant=None, owner=None, distroseries=None,
+ name=None, branch=None, git_ref=None,
+ require_virtualized=True, date_created=DEFAULT):
+ """Make a new Snap."""
+ if registrant is None:
+ registrant = self.makePerson()
+ if owner is None:
+ owner = self.makeTeam(registrant)
+ if distroseries is None:
+ distroseries = self.makeDistroSeries()
+ if name is None:
+ name = self.getUniqueString(u"snap-name")
+ kwargs = {}
+ if branch is None and git_ref is None:
+ branch = self.makeAnyBranch()
+ if branch is not None:
+ kwargs["branch"] = branch
+ elif git_ref is not None:
+ kwargs["git_repository"] = git_ref.repository
+ kwargs["git_path"] = git_ref.path
+ snap = getUtility(ISnapSet).new(
+ registrant, owner, distroseries, name,
+ require_virtualized=require_virtualized, date_created=date_created,
+ **kwargs)
+ IStore(snap).flush()
+ return snap
+
# Some factory methods return simple Python types. We don't add
# security wrappers for them, as well as for objects created by
=== modified file 'utilities/snakefood/lp-sfood-packages'
--- utilities/snakefood/lp-sfood-packages 2015-01-13 14:07:42 +0000
+++ utilities/snakefood/lp-sfood-packages 2015-07-23 14:38:45 +0000
@@ -2,6 +2,7 @@
lp/testopenid
lp/testing
lp/soyuz
+lp/snappy
lp/services
lp/scripts
lp/registry
Follow ups