Merge ~andrey-fedoseev/launchpad:snap-base-features into launchpad:master

Proposed by Andrey Fedoseev
Status: Merged
Approved by: Andrey Fedoseev
Approved revision: 6f32d905743ce2f84b8ef18f2b674485785c1862
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~andrey-fedoseev/launchpad:snap-base-features
Merge into: launchpad:master
Diff against target: 307 lines (+121/-2)
4 files modified
lib/lp/snappy/interfaces/snapbase.py (+36/-2)
lib/lp/snappy/model/snapbase.py (+29/-0)
lib/lp/snappy/tests/test_snapbase.py (+54/-0)
lib/lp/testing/factory.py (+2/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+429878@code.launchpad.net

Commit message

Add `ISnapBase.features` field

Description of the change

The field is used to designate the features available for a snap base

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/snappy/interfaces/snapbase.py b/lib/lp/snappy/interfaces/snapbase.py
2index 08cfd79..60ebeb5 100644
3--- a/lib/lp/snappy/interfaces/snapbase.py
4+++ b/lib/lp/snappy/interfaces/snapbase.py
5@@ -8,10 +8,13 @@ __all__ = [
6 "ISnapBase",
7 "ISnapBaseSet",
8 "NoSuchSnapBase",
9+ "SnapBaseFeature",
10 ]
11
12+
13 import http.client
14
15+from lazr.enum import EnumeratedType, Item
16 from lazr.restful.declarations import (
17 REQUEST_USER,
18 call_with,
19@@ -33,7 +36,7 @@ from lazr.restful.fields import CollectionField, Reference
20 from lazr.restful.interface import copy_field
21 from zope.component import getUtility
22 from zope.interface import Interface
23-from zope.schema import Bool, Datetime, Dict, Int, List, TextLine
24+from zope.schema import Bool, Choice, Datetime, Dict, Int, List, TextLine
25
26 from lp import _
27 from lp.app.errors import NameLookupFailed
28@@ -74,6 +77,14 @@ class SnapBaseNameField(ContentNameField):
29 return None
30
31
32+class SnapBaseFeature(EnumeratedType):
33+ ALLOW_DUPLICATE_BUILD_ON = Item(
34+ "allow_duplicate_build_on",
35+ description="Allow items with duplicate build-on attribute in the "
36+ "list of architectures",
37+ )
38+
39+
40 class ISnapBaseView(Interface):
41 """`ISnapBase` attributes that anyone can view."""
42
43@@ -181,6 +192,21 @@ class ISnapBaseEditableAttributes(Interface):
44 )
45 )
46
47+ features = exported(
48+ Dict(
49+ title=_("Features supported by this base"),
50+ key_type=Choice(vocabulary=SnapBaseFeature),
51+ value_type=Bool(),
52+ required=False,
53+ readonly=False,
54+ description=_(
55+ "A dictionary designating the features supported by the base. "
56+ "Key is the name of a feature, value is a boolean indicating "
57+ "whether the feature is supported or not."
58+ ),
59+ )
60+ )
61+
62
63 class ISnapBaseEdit(Interface):
64 """`ISnapBase` methods that require launchpad.Edit permission."""
65@@ -263,7 +289,14 @@ class ISnapBaseSetEdit(Interface):
66 )
67 )
68 @export_factory_operation(
69- ISnapBase, ["name", "display_name", "distro_series", "build_channels"]
70+ ISnapBase,
71+ [
72+ "name",
73+ "display_name",
74+ "distro_series",
75+ "build_channels",
76+ "features",
77+ ],
78 )
79 @operation_for_version("devel")
80 def new(
81@@ -272,6 +305,7 @@ class ISnapBaseSetEdit(Interface):
82 display_name,
83 distro_series,
84 build_channels,
85+ features,
86 processors=None,
87 date_created=None,
88 ):
89diff --git a/lib/lp/snappy/model/snapbase.py b/lib/lp/snappy/model/snapbase.py
90index f87a3e6..b6e8b33 100644
91--- a/lib/lp/snappy/model/snapbase.py
92+++ b/lib/lp/snappy/model/snapbase.py
93@@ -7,7 +7,11 @@ __all__ = [
94 "SnapBase",
95 ]
96
97+from typing import Dict, Optional
98+
99 import pytz
100+from lazr.enum import Item
101+from storm.databases.postgres import JSON as PgJSON
102 from storm.locals import (
103 JSON,
104 Bool,
105@@ -33,6 +37,7 @@ from lp.snappy.interfaces.snapbase import (
106 ISnapBase,
107 ISnapBaseSet,
108 NoSuchSnapBase,
109+ SnapBaseFeature,
110 )
111 from lp.soyuz.interfaces.archive import (
112 ArchiveDependencyError,
113@@ -69,6 +74,8 @@ class SnapBase(Storm):
114
115 is_default = Bool(name="is_default", allow_none=False)
116
117+ _features = PgJSON(name="features", allow_none=False)
118+
119 def __init__(
120 self,
121 registrant,
122@@ -76,6 +83,7 @@ class SnapBase(Storm):
123 display_name,
124 distro_series,
125 build_channels,
126+ features: Optional[Dict[Item, bool]],
127 date_created=DEFAULT,
128 ):
129 super().__init__()
130@@ -85,8 +93,27 @@ class SnapBase(Storm):
131 self.distro_series = distro_series
132 self.build_channels = build_channels
133 self.date_created = date_created
134+ self.features = features
135 self.is_default = False
136
137+ @property
138+ def features(self) -> Dict[Item, bool]:
139+ features = {}
140+ for token, is_enabled in self._features.items():
141+ try:
142+ term = SnapBaseFeature.getTermByToken(token)
143+ except LookupError:
144+ continue
145+ features[term.value] = is_enabled
146+ return features
147+
148+ @features.setter
149+ def features(self, value: Optional[Dict[Item, bool]]) -> None:
150+ features = {}
151+ for item, is_enabled in (value or {}).items():
152+ features[item.title] = is_enabled
153+ self._features = features
154+
155 def _getProcessors(self):
156 return list(
157 Store.of(self).find(
158@@ -217,6 +244,7 @@ class SnapBaseSet:
159 display_name,
160 distro_series,
161 build_channels,
162+ features,
163 processors=None,
164 date_created=DEFAULT,
165 ):
166@@ -228,6 +256,7 @@ class SnapBaseSet:
167 display_name,
168 distro_series,
169 build_channels,
170+ features,
171 date_created=date_created,
172 )
173 store.add(snap_base)
174diff --git a/lib/lp/snappy/tests/test_snapbase.py b/lib/lp/snappy/tests/test_snapbase.py
175index afd71c1..978e8b6 100644
176--- a/lib/lp/snappy/tests/test_snapbase.py
177+++ b/lib/lp/snappy/tests/test_snapbase.py
178@@ -12,6 +12,7 @@ from testtools.matchers import (
179 MatchesStructure,
180 )
181 from zope.component import getAdapter, getUtility
182+from zope.security.proxy import removeSecurityProxy
183
184 from lp.app.interfaces.security import IAuthorization
185 from lp.registry.interfaces.pocket import PackagePublishingPocket
186@@ -21,6 +22,7 @@ from lp.snappy.interfaces.snapbase import (
187 ISnapBase,
188 ISnapBaseSet,
189 NoSuchSnapBase,
190+ SnapBaseFeature,
191 )
192 from lp.soyuz.interfaces.component import IComponentSet
193 from lp.testing import (
194@@ -69,6 +71,28 @@ class TestSnapBase(TestCaseWithFactory):
195 getUtility(ISnapBaseSet).setDefault(snap_base)
196 self.assertRaises(CannotDeleteSnapBase, snap_base.destroySelf)
197
198+ def test_features(self):
199+ snap_base = self.factory.makeSnapBase(
200+ features={SnapBaseFeature.ALLOW_DUPLICATE_BUILD_ON: True}
201+ )
202+
203+ # feature are saved to the database by their title
204+ self.assertEqual(
205+ {
206+ "allow_duplicate_build_on": True,
207+ },
208+ removeSecurityProxy(snap_base)._features,
209+ )
210+
211+ # pretend that the database contains an invalid value
212+ removeSecurityProxy(snap_base)._features["unknown_feature"] = True
213+ self.assertEqual(
214+ {
215+ SnapBaseFeature.ALLOW_DUPLICATE_BUILD_ON: True,
216+ },
217+ snap_base.features,
218+ )
219+
220
221 class TestSnapBaseProcessors(TestCaseWithFactory):
222
223@@ -102,6 +126,7 @@ class TestSnapBaseProcessors(TestCaseWithFactory):
224 display_name=self.factory.getUniqueUnicode(),
225 distro_series=self.distroseries,
226 build_channels={},
227+ features={},
228 )
229 self.assertContentEqual(self.procs, snap_base.processors)
230
231@@ -113,6 +138,7 @@ class TestSnapBaseProcessors(TestCaseWithFactory):
232 display_name=self.factory.getUniqueUnicode(),
233 distro_series=self.distroseries,
234 build_channels={},
235+ features={},
236 processors=self.procs[:2],
237 )
238 self.assertContentEqual(self.procs[:2], snap_base.processors)
239@@ -211,6 +237,7 @@ class TestSnapBaseWebservice(TestCaseWithFactory):
240 display_name="Dummy",
241 distro_series=distroseries_url,
242 build_channels={"snapcraft": "stable"},
243+ features={"allow_duplicate_build_on": True},
244 )
245 self.assertEqual(201, response.status)
246 snap_base = webservice.get(response.getHeader("Location")).jsonBody()
247@@ -228,6 +255,7 @@ class TestSnapBaseWebservice(TestCaseWithFactory):
248 webservice.getAbsoluteUrl(distroseries_url)
249 ),
250 "build_channels": Equals({"snapcraft": "stable"}),
251+ "features": Equals({"allow_duplicate_build_on": True}),
252 "is_default": Is(False),
253 }
254 ),
255@@ -265,6 +293,32 @@ class TestSnapBaseWebservice(TestCaseWithFactory):
256 b"name: dummy is already in use by another base.", response.body
257 )
258
259+ def test_new_invalid_features(self):
260+ person = self.factory.makeRegistryExpert()
261+ distroseries = self.factory.makeDistroSeries()
262+ distroseries_url = api_url(distroseries)
263+ webservice = webservice_for_person(
264+ person, permission=OAuthPermission.WRITE_PUBLIC
265+ )
266+ webservice.default_api_version = "devel"
267+ logout()
268+ response = webservice.named_post(
269+ "/+snap-bases",
270+ "new",
271+ name="dummy",
272+ display_name="Dummy",
273+ distro_series=distroseries_url,
274+ build_channels={"snapcraft": "stable"},
275+ features={
276+ "allow_duplicate_build_on": True,
277+ "invalid_feature": True,
278+ },
279+ )
280+ self.assertEqual(400, response.status)
281+ self.assertStartsWith(
282+ response.body, b'features: Invalid value "invalid_feature".'
283+ )
284+
285 def test_getByName(self):
286 # lp.snap_bases.getByName returns a matching SnapBase.
287 person = self.factory.makePerson()
288diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
289index e70aef7..7ecb210 100644
290--- a/lib/lp/testing/factory.py
291+++ b/lib/lp/testing/factory.py
292@@ -6392,6 +6392,7 @@ class LaunchpadObjectFactory(ObjectFactory):
293 display_name=None,
294 distro_series=None,
295 build_channels=None,
296+ features=None,
297 processors=None,
298 date_created=DEFAULT,
299 ):
300@@ -6414,6 +6415,7 @@ class LaunchpadObjectFactory(ObjectFactory):
301 display_name,
302 distro_series,
303 build_channels,
304+ features=features,
305 processors=processors,
306 date_created=date_created,
307 )

Subscribers

People subscribed via source and target branches

to status/vote changes: