Merge ~jslarraz/review-tools:container-add-get-yaml into review-tools:master
- Git
- lp:~jslarraz/review-tools
- container-add-get-yaml
- Merge into master
Status: | Merged |
---|---|
Merged at revision: | 7dcfef8c2cb776b042f606ca2db502fc05dd787c |
Proposed branch: | ~jslarraz/review-tools:container-add-get-yaml |
Merge into: | review-tools:master |
Diff against target: |
431 lines (+159/-124) 6 files modified
dev/null (+0/-38) reviewtools/containers/base_container.py (+43/-1) reviewtools/sr_common.py (+10/-57) reviewtools/sr_tests.py (+12/-23) reviewtools/tests/containers/test_base_container.py (+93/-3) reviewtools/tests/test_bbb_example_sr_skeleton.py (+1/-2) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alex Murray | Approve | ||
Review via email: mp+466662@code.launchpad.net |
Commit message
many: add get_yaml to BaseContainer and use it to read snap.yaml and manifest.yaml
Description of the change
Jorge Sancho Larraz (jslarraz) wrote : | # |
Alex Murray (alexmurray) wrote : | # |
LGTM although I wonder a bit about hard-coding the duplicate keys logic and also perhaps since this method is wrapping get_file() then perhaps it should be called get_file_as_yaml() or something to indicate this...? although naming is always hard.
I am happy for you to merge this as is but would be interested to know your thoughts either way.
Jorge Sancho Larraz (jslarraz) wrote (last edit ): | # |
Regarding the naming, I don't think that the function name should indicate that it wraps `get_file` as it is an implementation detail that should be transparent for anyone using this function. Otherwise, changing the implementation would require to update the function name, what would break backwards compatibility. It should probable be better included in the documentation. That said, I'll be happy to update the name to `get_file_as_yaml` as it makes more evident that we are actually reading a file from the container and interpreting it as yaml.
Regarding duplicated keys, I just hard-coded them because yaml specification does not allow them and all cases where I started to use the `get_yaml` function, previously checked for duplicated keys with the function `_verify_
Working on that!
Jorge Sancho Larraz (jslarraz) wrote : | # |
Alex Murray (alexmurray) wrote : | # |
Thanks @jslarraz - still LGTM - fwiw I like the parameterised approach - very nice!
Preview Diff
1 | diff --git a/reviewtools/containers/base_container.py b/reviewtools/containers/base_container.py |
2 | index 0d51021..3c4555f 100644 |
3 | --- a/reviewtools/containers/base_container.py |
4 | +++ b/reviewtools/containers/base_container.py |
5 | @@ -1,6 +1,7 @@ |
6 | import os |
7 | import shutil |
8 | import magic |
9 | +import ruamel.yaml |
10 | from typing import Union |
11 | from tempfile import mkdtemp |
12 | |
13 | @@ -244,7 +245,7 @@ class BaseContainer: |
14 | """ |
15 | raise NotImplementedError("Must be implemented by subclasses") |
16 | |
17 | - def get_file(self, fn: str) -> Union[str, None]: |
18 | + def get_file(self, fn: str) -> bytes: |
19 | """ |
20 | Reads and return the raw content of the specified file. |
21 | |
22 | @@ -265,6 +266,47 @@ class BaseContainer: |
23 | with open(os.path.join(self.unpack_dir, fn), "rb") as fd: |
24 | return fd.read() |
25 | |
26 | + def get_file_as_yaml( |
27 | + self, fn: str, yaml_version: str = "1.2", allow_duplicate_keys: bool = False |
28 | + ) -> Union[dict, None]: |
29 | + """ |
30 | + Reads a file and tries to parse it as a yaml file. It uses get_file() to |
31 | + read the specified file from the container. |
32 | + |
33 | + Args: |
34 | + fn (str): file path relative to the container root (i.e. unpack_dir) |
35 | + yaml_version (str): yaml specification version used to parse the file. |
36 | + Defaults to ``1.2`` |
37 | + allow_duplicate_keys (bool): if ``True``, allow duplicate keys in the |
38 | + file. Defaults to ``False`` |
39 | + |
40 | + Returns: |
41 | + dict: file content parsed as yaml |
42 | + |
43 | + Raises: |
44 | + ContainerException: Calling this method will raise an exception if |
45 | + the requested file is not found. |
46 | + """ |
47 | + # Check parameters |
48 | + if yaml_version not in ["1.1", "1.2"]: |
49 | + raise ContainerException("yaml_version must be 1.1 or 1.2") |
50 | + |
51 | + # Load file |
52 | + file = self.get_file(fn) |
53 | + |
54 | + # Prevent duplicated keys |
55 | + yaml = ruamel.yaml.YAML(typ="safe") |
56 | + yaml.allow_duplicate_keys = allow_duplicate_keys |
57 | + # Octal integers, used by layout::mode are handle differently in yaml |
58 | + # versions 1.1 (0755) and 1.2 (0o755). |
59 | + yaml.version = yaml_version |
60 | + try: |
61 | + yaml_object = yaml.load(file.decode()) |
62 | + except ruamel.yaml.constructor.DuplicateKeyError as e: |
63 | + raise ContainerException(e.problem) |
64 | + |
65 | + return yaml_object |
66 | + |
67 | def get_files_list(self, abs: bool = False) -> list: |
68 | """ |
69 | Returns a list of files present in the container. |
70 | diff --git a/reviewtools/sr_common.py b/reviewtools/sr_common.py |
71 | index 9b961e2..5b2f2ec 100644 |
72 | --- a/reviewtools/sr_common.py |
73 | +++ b/reviewtools/sr_common.py |
74 | @@ -17,14 +17,11 @@ |
75 | from __future__ import print_function |
76 | import os |
77 | import re |
78 | -import yaml |
79 | - |
80 | |
81 | from reviewtools.common import ( |
82 | ReviewBase, |
83 | ReviewException, |
84 | error, |
85 | - open_file_read, |
86 | read_snapd_base_declaration, |
87 | unsquashfs_lln, |
88 | unsquashfs_lln_parse, |
89 | @@ -408,32 +405,17 @@ class SnapReview(ReviewBase): |
90 | self.pkg_bin_files = self.snap.get_compiled_binaries_list(abs=True) |
91 | self.tmp_dir = self.snap.tmp_dir |
92 | |
93 | - snap_yaml = self._extract_snap_yaml() |
94 | - try: |
95 | - self.snap_yaml = yaml.safe_load(snap_yaml) |
96 | - except Exception: # pragma: nocover |
97 | - error("Could not load snap.yaml. Is it properly formatted?") |
98 | - snap_yaml.close() |
99 | - |
100 | - # read yaml again for duplicated keys check (py-yaml does not |
101 | - # do that) |
102 | - snap_yaml = self._extract_snap_yaml() |
103 | try: |
104 | - self._verify_no_duplicated_yaml_keys(snap_yaml.read()) |
105 | - except Exception as e: |
106 | - error("Found duplicated yaml keys in snap.yaml: %s" % e) |
107 | - snap_yaml.close() |
108 | - |
109 | - self.snap_manifest_yaml = {} |
110 | - manifest_yaml = self._extract_snap_manifest_yaml() |
111 | - if manifest_yaml is not None: |
112 | - try: |
113 | - self.snap_manifest_yaml = yaml.safe_load(manifest_yaml) |
114 | - manifest_yaml.close() |
115 | - if self.snap_manifest_yaml is None: |
116 | - self.snap_manifest_yaml = {} |
117 | - except Exception: # pragma: nocover |
118 | - error("Could not load snap/manifest.yaml. Is it properly " "formatted?") |
119 | + self.snap_yaml = self.snap.get_file_as_yaml( |
120 | + "meta/snap.yaml", yaml_version="1.1" |
121 | + ) |
122 | + self.snap_manifest_yaml = {} |
123 | + if "snap/manifest.yaml" in self.snap.files: |
124 | + self.snap_manifest_yaml = self.snap.get_file_as_yaml( |
125 | + "snap/manifest.yaml", yaml_version="1.1" |
126 | + ) |
127 | + except ContainerException as e: |
128 | + error(str(e)) |
129 | |
130 | ( |
131 | self.base_declaration_series, |
132 | @@ -522,24 +504,6 @@ class SnapReview(ReviewBase): |
133 | # cache unsquashfs -lln so we can use it all over |
134 | self.unsquashfs_lln_hdr, self.unsquashfs_lln_entries = self._unsquashfs_lln(fn) |
135 | |
136 | - # Since coverage is looked at via the testsuite and the testsuite mocks |
137 | - # this out, don't cover this |
138 | - def _extract_snap_yaml(self): # pragma: nocover |
139 | - """Extract and read the snappy 16.04 snap.yaml""" |
140 | - y = os.path.join(self.unpack_dir, "meta/snap.yaml") |
141 | - if not os.path.isfile(y): |
142 | - error("Could not find snap.yaml.") |
143 | - return open_file_read(y) |
144 | - |
145 | - # Since coverage is looked at via the testsuite and the testsuite mocks |
146 | - # this out, don't cover this |
147 | - def _extract_snap_manifest_yaml(self): # pragma: nocover |
148 | - """Extract and read snap/manifest.yaml if it exists""" |
149 | - y = os.path.join(self.unpack_dir, "snap/manifest.yaml") |
150 | - if not os.path.isfile(y): |
151 | - return None |
152 | - return open_file_read(y) |
153 | - |
154 | def _unsquashfs_lln(self, snap_pkg): |
155 | """Run unsquashfs -lln on a snap package""" |
156 | (rc, out) = unsquashfs_lln(snap_pkg) |
157 | @@ -632,14 +596,3 @@ class SnapReview(ReviewBase): |
158 | if os.path.getsize(fn) <= size: |
159 | return True |
160 | return False |
161 | - |
162 | - def _verify_no_duplicated_yaml_keys(self, raw_snap_yaml): |
163 | - """Verify there are no duplicated yaml map keys""" |
164 | - import ruamel.yaml |
165 | - |
166 | - yaml = ruamel.yaml.YAML(typ="safe") |
167 | - yaml.allow_duplicate_keys = False |
168 | - try: |
169 | - yaml.load(raw_snap_yaml) |
170 | - except ruamel.yaml.constructor.DuplicateKeyError as e: |
171 | - raise SnapReviewException(e.problem) |
172 | diff --git a/reviewtools/sr_tests.py b/reviewtools/sr_tests.py |
173 | index bfbf375..eaea513 100644 |
174 | --- a/reviewtools/sr_tests.py |
175 | +++ b/reviewtools/sr_tests.py |
176 | @@ -15,9 +15,7 @@ |
177 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
178 | |
179 | import copy |
180 | -import io |
181 | import os |
182 | -import yaml |
183 | |
184 | from unittest.mock import patch |
185 | from unittest import TestCase |
186 | @@ -46,19 +44,17 @@ class _BaseContainer: |
187 | def __init__(self, fn): |
188 | self.filename = fn |
189 | self._unpack_dir = ".".join(self.filename.split(".")[:-1]) |
190 | - self._files = [] |
191 | + self._files = ["meta/snap.yaml", "snap/manifest.yaml"] |
192 | self._bin_files = [] |
193 | self.tmp_dir = "/faketmpdir" |
194 | |
195 | - |
196 | -def _extract_snap_yaml(self): |
197 | - """Pretend we read the snap.yaml file""" |
198 | - return io.StringIO(TEST_SNAP_YAML) |
199 | - |
200 | - |
201 | -def _extract_snap_manifest_yaml(self): |
202 | - """Pretend we read the snap/manifest.yaml file""" |
203 | - return io.StringIO(TEST_SNAP_MANIFEST_YAML) |
204 | + def get_file_as_yaml(self, fn, yaml_version="1.2", allow_duplicate_keys=False): |
205 | + if fn == "meta/snap.yaml": |
206 | + return TEST_SNAP_YAML |
207 | + elif fn == "snap/manifest.yaml": |
208 | + return TEST_SNAP_MANIFEST_YAML |
209 | + else: |
210 | + return {} |
211 | |
212 | |
213 | def _pkgfmt_type(self): |
214 | @@ -90,12 +86,9 @@ def create_patches(): |
215 | patch("reviewtools.sr_common.SnapContainer.__init__", _BaseContainer.__init__) |
216 | ) |
217 | patches.append( |
218 | - patch("reviewtools.sr_common.SnapReview._extract_snap_yaml", _extract_snap_yaml) |
219 | - ) |
220 | - patches.append( |
221 | patch( |
222 | - "reviewtools.sr_common.SnapReview._extract_snap_manifest_yaml", |
223 | - _extract_snap_manifest_yaml, |
224 | + "reviewtools.sr_common.SnapContainer.get_file_as_yaml", |
225 | + _BaseContainer.get_file_as_yaml, |
226 | ) |
227 | ) |
228 | # sr_common |
229 | @@ -141,15 +134,11 @@ class TestSnapReview(TestCase): |
230 | |
231 | def _update_test_snap_yaml(self): |
232 | global TEST_SNAP_YAML |
233 | - TEST_SNAP_YAML = yaml.dump( |
234 | - self.test_snap_yaml, default_flow_style=False, indent=4 |
235 | - ) |
236 | + TEST_SNAP_YAML = self.test_snap_yaml |
237 | |
238 | def _update_test_snap_manifest_yaml(self): |
239 | global TEST_SNAP_MANIFEST_YAML |
240 | - TEST_SNAP_MANIFEST_YAML = yaml.dump( |
241 | - self.test_snap_manifest_yaml, default_flow_style=False, indent=4 |
242 | - ) |
243 | + TEST_SNAP_MANIFEST_YAML = self.test_snap_manifest_yaml |
244 | |
245 | def _update_test_name(self): |
246 | self.test_name = "%s.origin_%s_%s.snap" % ( |
247 | diff --git a/reviewtools/tests/containers/test_base_container.py b/reviewtools/tests/containers/test_base_container.py |
248 | index 1b4e022..277be8f 100644 |
249 | --- a/reviewtools/tests/containers/test_base_container.py |
250 | +++ b/reviewtools/tests/containers/test_base_container.py |
251 | @@ -35,7 +35,15 @@ class TestBaseContainer(unittest.TestCase): |
252 | os.makedirs(os.path.dirname(unpacked_file), exist_ok=True) |
253 | if file not in self.expected_binaries: |
254 | with open(unpacked_file, "w+b") as fd: |
255 | - fd.write(b"Test") |
256 | + if file == "meta/snap.yaml": |
257 | + fd.write( |
258 | + b""" |
259 | +map: |
260 | + key: value |
261 | + """ |
262 | + ) |
263 | + else: |
264 | + fd.write(b"Test") |
265 | else: |
266 | shutil.copyfile(os.path.join("/", file), unpacked_file) |
267 | return unpack_dir |
268 | @@ -126,10 +134,10 @@ class TestBaseContainer(unittest.TestCase): |
269 | try: |
270 | container = BaseContainer(self.fn) |
271 | container._unpack_dir = self.unpack_dir |
272 | - snap_yaml = container.get_file("meta/snap.yaml") |
273 | + snap_yaml = container.get_file("meta/icon.png") |
274 | self.assertEqual(snap_yaml.decode(), "Test") |
275 | except ContainerException: |
276 | - self.fail("An unexpected error occurred while getting meta/snap.yaml") |
277 | + self.fail("An unexpected error occurred while getting meta/icon.png") |
278 | |
279 | def test_get_file__missing_file(self): |
280 | with self.assertRaises(ContainerException) as context: |
281 | @@ -138,6 +146,88 @@ class TestBaseContainer(unittest.TestCase): |
282 | container.get_file("non/existent/file") |
283 | self.assertTrue("Could not find" in context.exception.value) |
284 | |
285 | + # Test get yaml |
286 | + def test_get_file_as_yaml__happy(self): |
287 | + try: |
288 | + container = BaseContainer(self.fn) |
289 | + container._unpack_dir = self.unpack_dir |
290 | + snap_yaml = container.get_file_as_yaml("meta/snap.yaml") |
291 | + self.assertEqual(snap_yaml, {"map": {"key": "value"}}) |
292 | + except ContainerException: |
293 | + self.fail("An unexpected error occurred while getting meta/snap.yaml") |
294 | + |
295 | + def test_get_file_as_yaml__dup_keys(self): |
296 | + # Write the file to test |
297 | + with open(os.path.join(self.unpack_dir, "test.yaml"), "w+b") as fd: |
298 | + fd.write( |
299 | + b""" |
300 | +map: |
301 | + key: value1 |
302 | + key: value2 |
303 | + """ |
304 | + ) |
305 | + |
306 | + with self.assertRaises(ContainerException) as context: |
307 | + container = BaseContainer(self.fn) |
308 | + container._unpack_dir = self.unpack_dir |
309 | + container.get_file_as_yaml("test.yaml") |
310 | + self.assertTrue('found duplicate key "key"' in context.exception.value) |
311 | + |
312 | + # Clean the tested file |
313 | + os.unlink(os.path.join(self.unpack_dir, "test.yaml")) |
314 | + |
315 | + def test_get_file_as_yaml__allow_dup_keys(self): |
316 | + # Write the file to test |
317 | + with open(os.path.join(self.unpack_dir, "test.yaml"), "w+b") as fd: |
318 | + fd.write( |
319 | + b""" |
320 | +map: |
321 | + key: value1 |
322 | + key: value2 |
323 | + """ |
324 | + ) |
325 | + |
326 | + container = BaseContainer(self.fn) |
327 | + container._unpack_dir = self.unpack_dir |
328 | + container.get_file_as_yaml("test.yaml", allow_duplicate_keys=True) |
329 | + |
330 | + # Clean the tested file |
331 | + os.unlink(os.path.join(self.unpack_dir, "test.yaml")) |
332 | + |
333 | + def test_get_file_as_yaml__version_1_1(self): |
334 | + # Write the file to test |
335 | + with open(os.path.join(self.unpack_dir, "test.yaml"), "w+b") as fd: |
336 | + fd.write( |
337 | + b""" |
338 | +mode: 0777 |
339 | + """ |
340 | + ) |
341 | + |
342 | + container = BaseContainer(self.fn) |
343 | + container._unpack_dir = self.unpack_dir |
344 | + test_yaml = container.get_file_as_yaml("test.yaml", yaml_version="1.1") |
345 | + self.assertEqual(test_yaml["mode"], 511) |
346 | + |
347 | + # Clean the tested file |
348 | + os.unlink(os.path.join(self.unpack_dir, "test.yaml")) |
349 | + |
350 | + def test_get_file_as_yaml__version_1_2(self): |
351 | + # Write the file to test |
352 | + with open(os.path.join(self.unpack_dir, "test.yaml"), "w+b") as fd: |
353 | + fd.write( |
354 | + b""" |
355 | +mode: 0777 |
356 | + """ |
357 | + ) |
358 | + |
359 | + container = BaseContainer(self.fn) |
360 | + container._unpack_dir = self.unpack_dir |
361 | + test_yaml = container.get_file_as_yaml("test.yaml") |
362 | + self.assertEqual(test_yaml["mode"], 777) |
363 | + |
364 | + # Clean the tested file |
365 | + os.unlink(os.path.join(self.unpack_dir, "test.yaml")) |
366 | + |
367 | # Test get files list |
368 | def test_get_files_list__happy(self): |
369 | try: |
370 | diff --git a/reviewtools/tests/test_bbb_example_sr_skeleton.py b/reviewtools/tests/test_bbb_example_sr_skeleton.py |
371 | index 5a304cd..0abf94e 100644 |
372 | --- a/reviewtools/tests/test_bbb_example_sr_skeleton.py |
373 | +++ b/reviewtools/tests/test_bbb_example_sr_skeleton.py |
374 | @@ -63,7 +63,6 @@ class TestSnapReviewSkeleton(sr_tests.TestSnapReview): |
375 | self._update_test_name() |
376 | |
377 | import pprint |
378 | - import yaml |
379 | |
380 | print( |
381 | """ |
382 | @@ -75,4 +74,4 @@ class TestSnapReviewSkeleton(sr_tests.TestSnapReview): |
383 | """ |
384 | % (self.test_name) |
385 | ) |
386 | - pprint.pprint(yaml.load(sr_tests.TEST_SNAP_YAML, Loader=yaml.SafeLoader)) |
387 | + pprint.pprint(sr_tests.TEST_SNAP_YAML) |
388 | diff --git a/reviewtools/tests/test_sr_common.py b/reviewtools/tests/test_sr_common.py |
389 | deleted file mode 100644 |
390 | index 541cea9..0000000 |
391 | --- a/reviewtools/tests/test_sr_common.py |
392 | +++ /dev/null |
393 | @@ -1,38 +0,0 @@ |
394 | -"""test_sr_common.py: tests for the sr_common module""" |
395 | -# |
396 | -# Copyright (C) 2018 Canonical Ltd. |
397 | -# |
398 | -# This program is free software: you can redistribute it and/or modify |
399 | -# it under the terms of the GNU General Public License as published by |
400 | -# the Free Software Foundation; version 3 of the License. |
401 | -# |
402 | -# This program is distributed in the hope that it will be useful, |
403 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
404 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
405 | -# GNU General Public License for more details. |
406 | -# |
407 | -# You should have received a copy of the GNU General Public License |
408 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
409 | - |
410 | -from reviewtools.sr_common import SnapReview |
411 | -import reviewtools.sr_tests as sr_tests |
412 | - |
413 | - |
414 | -class TestSnapReviewCommon(sr_tests.TestSnapReview): |
415 | - def setUp(self): |
416 | - super().setUp() |
417 | - self.review = SnapReview("app.snap", "sr_common_review_type") |
418 | - |
419 | - def test_no_duplicated_yaml_keys(self): |
420 | - """Check _no_duplicated_yaml_keys""" |
421 | - b1 = """ |
422 | -map: |
423 | - key: |
424 | - key: |
425 | - """ |
426 | - for ok in [ |
427 | - b1, |
428 | - ]: |
429 | - with self.assertRaises(Exception) as e: |
430 | - self.review._verify_no_duplicated_yaml_keys(ok) |
431 | - self.assertRegex(str(e.exception), r'found duplicate key "key".*') |
Add `get_yaml` function to base_container. This function will use ruamel.yaml to load a yaml file in the container and return the corresponding dictionary.
Using ruamel.yaml with `.allow_ duplicate_ keys = False` removes the need to perform this check separately.
A clean up has also been performed in sr_common using this new `get_yaml` function.