Merge lp:~cjwatson/launchpad/snap-parse-architectures into lp:launchpad
- snap-parse-architectures
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18718 | ||||
Proposed branch: | lp:~cjwatson/launchpad/snap-parse-architectures | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/snap-initial-name-bzr | ||||
Diff against target: |
420 lines (+407/-0) 2 files modified
lib/lp/snappy/adapters/buildarch.py (+176/-0) lib/lp/snappy/adapters/tests/test_buildarch.py (+231/-0) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-parse-architectures | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+347998@code.launchpad.net |
Commit message
Implement determine_
Description of the change
This is based on https:/
The code is quite significantly refactored from Kyle's version, to make it easier to fit into Launchpad.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added directory 'lib/lp/snappy/adapters' |
2 | === added file 'lib/lp/snappy/adapters/__init__.py' |
3 | === added file 'lib/lp/snappy/adapters/buildarch.py' |
4 | --- lib/lp/snappy/adapters/buildarch.py 1970-01-01 00:00:00 +0000 |
5 | +++ lib/lp/snappy/adapters/buildarch.py 2018-06-14 17:14:47 +0000 |
6 | @@ -0,0 +1,176 @@ |
7 | +# Copyright 2018 Canonical Ltd. This software is licensed under the |
8 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
9 | + |
10 | +from __future__ import absolute_import, print_function, unicode_literals |
11 | + |
12 | +__metaclass__ = type |
13 | +__all__ = [ |
14 | + 'determine_architectures_to_build', |
15 | + ] |
16 | + |
17 | +from collections import Counter |
18 | + |
19 | +import six |
20 | + |
21 | +from lp.services.helpers import english_list |
22 | + |
23 | + |
24 | +class SnapArchitecturesParserError(Exception): |
25 | + """Base class for all exceptions in this module.""" |
26 | + |
27 | + |
28 | +class MissingPropertyError(SnapArchitecturesParserError): |
29 | + """Error for when an expected property is not present in the YAML.""" |
30 | + |
31 | + def __init__(self, prop): |
32 | + super(MissingPropertyError, self).__init__( |
33 | + "Architecture specification is missing the {!r} property".format( |
34 | + prop)) |
35 | + self.property = prop |
36 | + |
37 | + |
38 | +class IncompatibleArchitecturesStyleError(SnapArchitecturesParserError): |
39 | + """Error for when architectures mix incompatible styles.""" |
40 | + |
41 | + def __init__(self): |
42 | + super(IncompatibleArchitecturesStyleError, self).__init__( |
43 | + "'architectures' must either be a list of strings or dicts, not " |
44 | + "both") |
45 | + |
46 | + |
47 | +class DuplicateBuildOnError(SnapArchitecturesParserError): |
48 | + """Error for when multiple `build-on`s include the same architecture.""" |
49 | + |
50 | + def __init__(self, duplicates): |
51 | + super(DuplicateBuildOnError, self).__init__( |
52 | + "{} {} present in the 'build-on' of multiple items".format( |
53 | + english_list(duplicates), |
54 | + "is" if len(duplicates) == 1 else "are")) |
55 | + |
56 | + |
57 | +class UnsupportedBuildOnError(SnapArchitecturesParserError): |
58 | + """Error for when a requested architecture is not supported.""" |
59 | + |
60 | + def __init__(self, build_on): |
61 | + super(UnsupportedBuildOnError, self).__init__( |
62 | + "build-on specifies no supported architectures: {!r}".format( |
63 | + build_on)) |
64 | + self.build_on = build_on |
65 | + |
66 | + |
67 | +class SnapArchitecture: |
68 | + """A single entry in the snapcraft.yaml 'architectures' list.""" |
69 | + |
70 | + def __init__(self, build_on, run_on=None, build_error=None): |
71 | + """Create a new architecture entry. |
72 | + |
73 | + :param build_on: string or list; build-on property from |
74 | + snapcraft.yaml. |
75 | + :param run_on: string or list; run-on property from snapcraft.yaml |
76 | + (defaults to build_on). |
77 | + :param build_error: string; build-error property from |
78 | + snapcraft.yaml. |
79 | + """ |
80 | + self.build_on = ( |
81 | + [build_on] if isinstance(build_on, six.string_types) else build_on) |
82 | + if run_on: |
83 | + self.run_on = ( |
84 | + [run_on] if isinstance(run_on, six.string_types) else run_on) |
85 | + else: |
86 | + self.run_on = self.build_on |
87 | + self.build_error = build_error |
88 | + |
89 | + @classmethod |
90 | + def from_dict(cls, properties): |
91 | + """Create a new architecture entry from a dict.""" |
92 | + try: |
93 | + build_on = properties["build-on"] |
94 | + except KeyError: |
95 | + raise MissingPropertyError("build-on") |
96 | + |
97 | + return cls( |
98 | + build_on=build_on, run_on=properties.get("run-on"), |
99 | + build_error=properties.get("build-error")) |
100 | + |
101 | + |
102 | +class SnapBuildInstance: |
103 | + """A single instance of a snap that should be built. |
104 | + |
105 | + It has two useful attributes: |
106 | + |
107 | + - architecture: The architecture tag that should be used to build the |
108 | + snap. |
109 | + - required: Whether or not failure to build should cause the entire |
110 | + set to fail. |
111 | + """ |
112 | + |
113 | + def __init__(self, architecture, supported_architectures): |
114 | + """Construct a new `SnapBuildInstance`. |
115 | + |
116 | + :param architecture: `SnapArchitecture` instance. |
117 | + :param supported_architectures: List of supported architectures, |
118 | + sorted by priority. |
119 | + """ |
120 | + try: |
121 | + self.architecture = next( |
122 | + arch for arch in supported_architectures |
123 | + if arch in architecture.build_on) |
124 | + except StopIteration: |
125 | + raise UnsupportedBuildOnError(architecture.build_on) |
126 | + |
127 | + self.required = architecture.build_error != "ignore" |
128 | + |
129 | + |
130 | +def determine_architectures_to_build(snapcraft_data, supported_arches): |
131 | + """Return a set of architectures to build based on snapcraft.yaml. |
132 | + |
133 | + :param snapcraft_data: A parsed snapcraft.yaml. |
134 | + :param supported_arches: An ordered list of all architecture tags that |
135 | + we can create builds for. |
136 | + :return: a set of `SnapBuildInstance`s. |
137 | + """ |
138 | + architectures_list = snapcraft_data.get("architectures") |
139 | + |
140 | + if architectures_list: |
141 | + # First, determine what style we're parsing. Is it a list of |
142 | + # strings or a list of dicts? |
143 | + if all(isinstance(a, six.string_types) for a in architectures_list): |
144 | + # If a list of strings (old style), then that's only a single |
145 | + # item. |
146 | + architectures = [SnapArchitecture(build_on=architectures_list)] |
147 | + elif all(isinstance(arch, dict) for arch in architectures_list): |
148 | + # If a list of dicts (new style), then that's multiple items. |
149 | + architectures = [ |
150 | + SnapArchitecture.from_dict(a) for a in architectures_list] |
151 | + else: |
152 | + # If a mix of both, bail. We can't reasonably handle it. |
153 | + raise IncompatibleArchitecturesStyleError() |
154 | + else: |
155 | + # If no architectures are specified, build one for each supported |
156 | + # architecture. |
157 | + architectures = [ |
158 | + SnapArchitecture(build_on=a) for a in supported_arches] |
159 | + |
160 | + # Ensure that multiple `build-on` items don't include the same |
161 | + # architecture; this is ambiguous and forbidden by snapcraft. Checking |
162 | + # this here means that we don't get duplicate supported_arch results |
163 | + # below. |
164 | + build_ons = Counter() |
165 | + for arch in architectures: |
166 | + build_ons.update(arch.build_on) |
167 | + duplicates = {arch for arch, count in build_ons.items() if count > 1} |
168 | + if duplicates: |
169 | + raise DuplicateBuildOnError(duplicates) |
170 | + |
171 | + architectures_to_build = set() |
172 | + for arch in architectures: |
173 | + try: |
174 | + architectures_to_build.add( |
175 | + SnapBuildInstance(arch, supported_arches)) |
176 | + except UnsupportedBuildOnError: |
177 | + # Snaps are allowed to declare that they build on architectures |
178 | + # that Launchpad doesn't currently support (perhaps they're |
179 | + # upcoming, or perhaps they used to be supported). We just |
180 | + # ignore those. |
181 | + pass |
182 | + return architectures_to_build |
183 | |
184 | === added directory 'lib/lp/snappy/adapters/tests' |
185 | === added file 'lib/lp/snappy/adapters/tests/__init__.py' |
186 | === added file 'lib/lp/snappy/adapters/tests/test_buildarch.py' |
187 | --- lib/lp/snappy/adapters/tests/test_buildarch.py 1970-01-01 00:00:00 +0000 |
188 | +++ lib/lp/snappy/adapters/tests/test_buildarch.py 2018-06-14 17:14:47 +0000 |
189 | @@ -0,0 +1,231 @@ |
190 | +# Copyright 2018 Canonical Ltd. This software is licensed under the |
191 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
192 | + |
193 | +from __future__ import absolute_import, print_function, unicode_literals |
194 | + |
195 | +__metaclass__ = type |
196 | + |
197 | +from testscenarios import ( |
198 | + load_tests_apply_scenarios, |
199 | + WithScenarios, |
200 | + ) |
201 | +from testtools.matchers import HasLength |
202 | + |
203 | +from lp.snappy.adapters.buildarch import ( |
204 | + determine_architectures_to_build, |
205 | + SnapArchitecture, |
206 | + SnapBuildInstance, |
207 | + UnsupportedBuildOnError, |
208 | + ) |
209 | +from lp.testing import TestCase |
210 | + |
211 | + |
212 | +class TestSnapArchitecture(WithScenarios, TestCase): |
213 | + |
214 | + scenarios = [ |
215 | + ("lists", { |
216 | + "architectures": {"build-on": ["amd64"], "run-on": ["amd64"]}, |
217 | + "expected_build_on": ["amd64"], |
218 | + "expected_run_on": ["amd64"], |
219 | + "expected_build_error": None, |
220 | + }), |
221 | + ("strings", { |
222 | + "architectures": {"build-on": "amd64", "run-on": "amd64"}, |
223 | + "expected_build_on": ["amd64"], |
224 | + "expected_run_on": ["amd64"], |
225 | + "expected_build_error": None, |
226 | + }), |
227 | + ("no run-on", { |
228 | + "architectures": {"build-on": ["amd64"]}, |
229 | + "expected_build_on": ["amd64"], |
230 | + "expected_run_on": ["amd64"], |
231 | + "expected_build_error": None, |
232 | + }), |
233 | + ("not required", { |
234 | + "architectures": { |
235 | + "build-on": ["amd64"], |
236 | + "run-on": "amd64", |
237 | + "build-error": "ignore"}, |
238 | + "expected_build_on": ["amd64"], |
239 | + "expected_run_on": ["amd64"], |
240 | + "expected_build_error": "ignore", |
241 | + }), |
242 | + ] |
243 | + |
244 | + def test_architecture(self): |
245 | + architecture = SnapArchitecture.from_dict(self.architectures) |
246 | + self.assertEqual(self.expected_build_on, architecture.build_on) |
247 | + self.assertEqual(self.expected_run_on, architecture.run_on) |
248 | + self.assertEqual(self.expected_build_error, architecture.build_error) |
249 | + |
250 | + |
251 | +class TestSnapBuildInstance(WithScenarios, TestCase): |
252 | + |
253 | + # Single-item scenarios taken from the architectures document: |
254 | + # https://forum.snapcraft.io/t/architectures/4972 |
255 | + scenarios = [ |
256 | + ("i386", { |
257 | + "architecture": SnapArchitecture( |
258 | + build_on="i386", run_on=["amd64", "i386"]), |
259 | + "supported_architectures": ["amd64", "i386", "armhf"], |
260 | + "expected_architecture": "i386", |
261 | + "expected_required": True, |
262 | + }), |
263 | + ("amd64", { |
264 | + "architecture": SnapArchitecture(build_on="amd64", run_on="all"), |
265 | + "supported_architectures": ["amd64", "i386", "armhf"], |
266 | + "expected_architecture": "amd64", |
267 | + "expected_required": True, |
268 | + }), |
269 | + ("amd64 priority", { |
270 | + "architecture": SnapArchitecture( |
271 | + build_on=["amd64", "i386"], run_on="all"), |
272 | + "supported_architectures": ["amd64", "i386", "armhf"], |
273 | + "expected_architecture": "amd64", |
274 | + "expected_required": True, |
275 | + }), |
276 | + ("i386 priority", { |
277 | + "architecture": SnapArchitecture( |
278 | + build_on=["amd64", "i386"], run_on="all"), |
279 | + "supported_architectures": ["i386", "amd64", "armhf"], |
280 | + "expected_architecture": "i386", |
281 | + "expected_required": True, |
282 | + }), |
283 | + ("optional", { |
284 | + "architecture": SnapArchitecture( |
285 | + build_on="amd64", build_error="ignore"), |
286 | + "supported_architectures": ["amd64", "i386", "armhf"], |
287 | + "expected_architecture": "amd64", |
288 | + "expected_required": False, |
289 | + }), |
290 | + ] |
291 | + |
292 | + def test_build_instance(self): |
293 | + instance = SnapBuildInstance( |
294 | + self.architecture, self.supported_architectures) |
295 | + self.assertEqual(self.expected_architecture, instance.architecture) |
296 | + self.assertEqual(self.expected_required, instance.required) |
297 | + |
298 | + |
299 | +class TestSnapBuildInstanceError(TestCase): |
300 | + |
301 | + def test_no_matching_arch_raises(self): |
302 | + architecture = SnapArchitecture(build_on="amd64", run_on="amd64") |
303 | + raised = self.assertRaises( |
304 | + UnsupportedBuildOnError, SnapBuildInstance, architecture, ["i386"]) |
305 | + self.assertEqual(["amd64"], raised.build_on) |
306 | + |
307 | + |
308 | +class TestDetermineArchitecturesToBuild(WithScenarios, TestCase): |
309 | + |
310 | + # Scenarios taken from the architectures document: |
311 | + # https://forum.snapcraft.io/t/architectures/4972 |
312 | + scenarios = [ |
313 | + ("none", { |
314 | + "architectures": None, |
315 | + "supported_architectures": ["amd64", "i386", "armhf"], |
316 | + "expected": [ |
317 | + {"architecture": "amd64", "required": True}, |
318 | + {"architecture": "i386", "required": True}, |
319 | + {"architecture": "armhf", "required": True}, |
320 | + ], |
321 | + }), |
322 | + ("i386", { |
323 | + "architectures": [ |
324 | + {"build-on": "i386", "run-on": ["amd64", "i386"]}, |
325 | + ], |
326 | + "supported_architectures": ["amd64", "i386", "armhf"], |
327 | + "expected": [{"architecture": "i386", "required": True}], |
328 | + }), |
329 | + ("amd64", { |
330 | + "architectures": [{"build-on": "amd64", "run-on": "all"}], |
331 | + "supported_architectures": ["amd64", "i386", "armhf"], |
332 | + "expected": [{"architecture": "amd64", "required": True}], |
333 | + }), |
334 | + ("amd64 and i386", { |
335 | + "architectures": [ |
336 | + {"build-on": "amd64", "run-on": "amd64"}, |
337 | + {"build-on": "i386", "run-on": "i386"}, |
338 | + ], |
339 | + "supported_architectures": ["amd64", "i386", "armhf"], |
340 | + "expected": [ |
341 | + {"architecture": "amd64", "required": True}, |
342 | + {"architecture": "i386", "required": True}, |
343 | + ], |
344 | + }), |
345 | + ("amd64 and i386 shorthand", { |
346 | + "architectures": [ |
347 | + {"build-on": "amd64"}, |
348 | + {"build-on": "i386"}, |
349 | + ], |
350 | + "supported_architectures": ["amd64", "i386", "armhf"], |
351 | + "expected": [ |
352 | + {"architecture": "amd64", "required": True}, |
353 | + {"architecture": "i386", "required": True}, |
354 | + ], |
355 | + }), |
356 | + ("amd64, i386, and armhf", { |
357 | + "architectures": [ |
358 | + {"build-on": "amd64", "run-on": "amd64"}, |
359 | + {"build-on": "i386", "run-on": "i386"}, |
360 | + { |
361 | + "build-on": "armhf", |
362 | + "run-on": "armhf", |
363 | + "build-error": "ignore", |
364 | + }, |
365 | + ], |
366 | + "supported_architectures": ["amd64", "i386", "armhf"], |
367 | + "expected": [ |
368 | + {"architecture": "amd64", "required": True}, |
369 | + {"architecture": "i386", "required": True}, |
370 | + {"architecture": "armhf", "required": False}, |
371 | + ], |
372 | + }), |
373 | + ("amd64 priority", { |
374 | + "architectures": [ |
375 | + {"build-on": ["amd64", "i386"], "run-on": "all"}, |
376 | + ], |
377 | + "supported_architectures": ["amd64", "i386", "armhf"], |
378 | + "expected": [{"architecture": "amd64", "required": True}], |
379 | + }), |
380 | + ("i386 priority", { |
381 | + "architectures": [ |
382 | + {"build-on": ["amd64", "i386"], "run-on": "all"}, |
383 | + ], |
384 | + "supported_architectures": ["i386", "amd64", "armhf"], |
385 | + "expected": [{"architecture": "i386", "required": True}], |
386 | + }), |
387 | + ("old style i386 priority", { |
388 | + "architectures": ["amd64", "i386"], |
389 | + "supported_architectures": ["i386", "amd64", "armhf"], |
390 | + "expected": [{"architecture": "i386", "required": True}], |
391 | + }), |
392 | + ("old style amd64 priority", { |
393 | + "architectures": ["amd64", "i386"], |
394 | + "supported_architectures": ["amd64", "i386", "armhf"], |
395 | + "expected": [{"architecture": "amd64", "required": True}], |
396 | + }), |
397 | + ("more architectures listed than are supported", { |
398 | + "architectures": [ |
399 | + {"build-on": "amd64"}, |
400 | + {"build-on": "i386"}, |
401 | + {"build-on": "armhf"}, |
402 | + ], |
403 | + "supported_architectures": ["amd64", "i386"], |
404 | + "expected": [ |
405 | + {"architecture": "amd64", "required": True}, |
406 | + {"architecture": "i386", "required": True}, |
407 | + ], |
408 | + }) |
409 | + ] |
410 | + |
411 | + def test_parser(self): |
412 | + snapcraft_data = {"architectures": self.architectures} |
413 | + build_instances = determine_architectures_to_build( |
414 | + snapcraft_data, self.supported_architectures) |
415 | + self.assertThat(build_instances, HasLength(len(self.expected))) |
416 | + for instance in build_instances: |
417 | + self.assertIn(instance.__dict__, self.expected) |
418 | + |
419 | + |
420 | +load_tests = load_tests_apply_scenarios |