Merge ~jslarraz/review-tools:container-add-get-yaml into review-tools:master

Proposed by Jorge Sancho Larraz
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)
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

To post a comment you must log in.
Revision history for this message
Jorge Sancho Larraz (jslarraz) wrote :

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.

Revision history for this message
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.

review: Approve
Revision history for this message
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_no_duplicated_yaml_keys`, but I agree that it should be better handled via function arguments.

Working on that!

Revision history for this message
Jorge Sancho Larraz (jslarraz) wrote :
Revision history for this message
Alex Murray (alexmurray) wrote :

Thanks @jslarraz - still LGTM - fwiw I like the parameterised approach - very nice!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/reviewtools/containers/base_container.py b/reviewtools/containers/base_container.py
index 0d51021..3c4555f 100644
--- a/reviewtools/containers/base_container.py
+++ b/reviewtools/containers/base_container.py
@@ -1,6 +1,7 @@
1import os1import os
2import shutil2import shutil
3import magic3import magic
4import ruamel.yaml
4from typing import Union5from typing import Union
5from tempfile import mkdtemp6from tempfile import mkdtemp
67
@@ -244,7 +245,7 @@ class BaseContainer:
244 """245 """
245 raise NotImplementedError("Must be implemented by subclasses")246 raise NotImplementedError("Must be implemented by subclasses")
246247
247 def get_file(self, fn: str) -> Union[str, None]:248 def get_file(self, fn: str) -> bytes:
248 """249 """
249 Reads and return the raw content of the specified file.250 Reads and return the raw content of the specified file.
250251
@@ -265,6 +266,47 @@ class BaseContainer:
265 with open(os.path.join(self.unpack_dir, fn), "rb") as fd:266 with open(os.path.join(self.unpack_dir, fn), "rb") as fd:
266 return fd.read()267 return fd.read()
267268
269 def get_file_as_yaml(
270 self, fn: str, yaml_version: str = "1.2", allow_duplicate_keys: bool = False
271 ) -> Union[dict, None]:
272 """
273 Reads a file and tries to parse it as a yaml file. It uses get_file() to
274 read the specified file from the container.
275
276 Args:
277 fn (str): file path relative to the container root (i.e. unpack_dir)
278 yaml_version (str): yaml specification version used to parse the file.
279 Defaults to ``1.2``
280 allow_duplicate_keys (bool): if ``True``, allow duplicate keys in the
281 file. Defaults to ``False``
282
283 Returns:
284 dict: file content parsed as yaml
285
286 Raises:
287 ContainerException: Calling this method will raise an exception if
288 the requested file is not found.
289 """
290 # Check parameters
291 if yaml_version not in ["1.1", "1.2"]:
292 raise ContainerException("yaml_version must be 1.1 or 1.2")
293
294 # Load file
295 file = self.get_file(fn)
296
297 # Prevent duplicated keys
298 yaml = ruamel.yaml.YAML(typ="safe")
299 yaml.allow_duplicate_keys = allow_duplicate_keys
300 # Octal integers, used by layout::mode are handle differently in yaml
301 # versions 1.1 (0755) and 1.2 (0o755).
302 yaml.version = yaml_version
303 try:
304 yaml_object = yaml.load(file.decode())
305 except ruamel.yaml.constructor.DuplicateKeyError as e:
306 raise ContainerException(e.problem)
307
308 return yaml_object
309
268 def get_files_list(self, abs: bool = False) -> list:310 def get_files_list(self, abs: bool = False) -> list:
269 """311 """
270 Returns a list of files present in the container.312 Returns a list of files present in the container.
diff --git a/reviewtools/sr_common.py b/reviewtools/sr_common.py
index 9b961e2..5b2f2ec 100644
--- a/reviewtools/sr_common.py
+++ b/reviewtools/sr_common.py
@@ -17,14 +17,11 @@
17from __future__ import print_function17from __future__ import print_function
18import os18import os
19import re19import re
20import yaml
21
2220
23from reviewtools.common import (21from reviewtools.common import (
24 ReviewBase,22 ReviewBase,
25 ReviewException,23 ReviewException,
26 error,24 error,
27 open_file_read,
28 read_snapd_base_declaration,25 read_snapd_base_declaration,
29 unsquashfs_lln,26 unsquashfs_lln,
30 unsquashfs_lln_parse,27 unsquashfs_lln_parse,
@@ -408,32 +405,17 @@ class SnapReview(ReviewBase):
408 self.pkg_bin_files = self.snap.get_compiled_binaries_list(abs=True)405 self.pkg_bin_files = self.snap.get_compiled_binaries_list(abs=True)
409 self.tmp_dir = self.snap.tmp_dir406 self.tmp_dir = self.snap.tmp_dir
410407
411 snap_yaml = self._extract_snap_yaml()
412 try:
413 self.snap_yaml = yaml.safe_load(snap_yaml)
414 except Exception: # pragma: nocover
415 error("Could not load snap.yaml. Is it properly formatted?")
416 snap_yaml.close()
417
418 # read yaml again for duplicated keys check (py-yaml does not
419 # do that)
420 snap_yaml = self._extract_snap_yaml()
421 try:408 try:
422 self._verify_no_duplicated_yaml_keys(snap_yaml.read())409 self.snap_yaml = self.snap.get_file_as_yaml(
423 except Exception as e:410 "meta/snap.yaml", yaml_version="1.1"
424 error("Found duplicated yaml keys in snap.yaml: %s" % e)411 )
425 snap_yaml.close()412 self.snap_manifest_yaml = {}
426413 if "snap/manifest.yaml" in self.snap.files:
427 self.snap_manifest_yaml = {}414 self.snap_manifest_yaml = self.snap.get_file_as_yaml(
428 manifest_yaml = self._extract_snap_manifest_yaml()415 "snap/manifest.yaml", yaml_version="1.1"
429 if manifest_yaml is not None:416 )
430 try:417 except ContainerException as e:
431 self.snap_manifest_yaml = yaml.safe_load(manifest_yaml)418 error(str(e))
432 manifest_yaml.close()
433 if self.snap_manifest_yaml is None:
434 self.snap_manifest_yaml = {}
435 except Exception: # pragma: nocover
436 error("Could not load snap/manifest.yaml. Is it properly " "formatted?")
437419
438 (420 (
439 self.base_declaration_series,421 self.base_declaration_series,
@@ -522,24 +504,6 @@ class SnapReview(ReviewBase):
522 # cache unsquashfs -lln so we can use it all over504 # cache unsquashfs -lln so we can use it all over
523 self.unsquashfs_lln_hdr, self.unsquashfs_lln_entries = self._unsquashfs_lln(fn)505 self.unsquashfs_lln_hdr, self.unsquashfs_lln_entries = self._unsquashfs_lln(fn)
524506
525 # Since coverage is looked at via the testsuite and the testsuite mocks
526 # this out, don't cover this
527 def _extract_snap_yaml(self): # pragma: nocover
528 """Extract and read the snappy 16.04 snap.yaml"""
529 y = os.path.join(self.unpack_dir, "meta/snap.yaml")
530 if not os.path.isfile(y):
531 error("Could not find snap.yaml.")
532 return open_file_read(y)
533
534 # Since coverage is looked at via the testsuite and the testsuite mocks
535 # this out, don't cover this
536 def _extract_snap_manifest_yaml(self): # pragma: nocover
537 """Extract and read snap/manifest.yaml if it exists"""
538 y = os.path.join(self.unpack_dir, "snap/manifest.yaml")
539 if not os.path.isfile(y):
540 return None
541 return open_file_read(y)
542
543 def _unsquashfs_lln(self, snap_pkg):507 def _unsquashfs_lln(self, snap_pkg):
544 """Run unsquashfs -lln on a snap package"""508 """Run unsquashfs -lln on a snap package"""
545 (rc, out) = unsquashfs_lln(snap_pkg)509 (rc, out) = unsquashfs_lln(snap_pkg)
@@ -632,14 +596,3 @@ class SnapReview(ReviewBase):
632 if os.path.getsize(fn) <= size:596 if os.path.getsize(fn) <= size:
633 return True597 return True
634 return False598 return False
635
636 def _verify_no_duplicated_yaml_keys(self, raw_snap_yaml):
637 """Verify there are no duplicated yaml map keys"""
638 import ruamel.yaml
639
640 yaml = ruamel.yaml.YAML(typ="safe")
641 yaml.allow_duplicate_keys = False
642 try:
643 yaml.load(raw_snap_yaml)
644 except ruamel.yaml.constructor.DuplicateKeyError as e:
645 raise SnapReviewException(e.problem)
diff --git a/reviewtools/sr_tests.py b/reviewtools/sr_tests.py
index bfbf375..eaea513 100644
--- a/reviewtools/sr_tests.py
+++ b/reviewtools/sr_tests.py
@@ -15,9 +15,7 @@
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17import copy17import copy
18import io
19import os18import os
20import yaml
2119
22from unittest.mock import patch20from unittest.mock import patch
23from unittest import TestCase21from unittest import TestCase
@@ -46,19 +44,17 @@ class _BaseContainer:
46 def __init__(self, fn):44 def __init__(self, fn):
47 self.filename = fn45 self.filename = fn
48 self._unpack_dir = ".".join(self.filename.split(".")[:-1])46 self._unpack_dir = ".".join(self.filename.split(".")[:-1])
49 self._files = []47 self._files = ["meta/snap.yaml", "snap/manifest.yaml"]
50 self._bin_files = []48 self._bin_files = []
51 self.tmp_dir = "/faketmpdir"49 self.tmp_dir = "/faketmpdir"
5250
5351 def get_file_as_yaml(self, fn, yaml_version="1.2", allow_duplicate_keys=False):
54def _extract_snap_yaml(self):52 if fn == "meta/snap.yaml":
55 """Pretend we read the snap.yaml file"""53 return TEST_SNAP_YAML
56 return io.StringIO(TEST_SNAP_YAML)54 elif fn == "snap/manifest.yaml":
5755 return TEST_SNAP_MANIFEST_YAML
5856 else:
59def _extract_snap_manifest_yaml(self):57 return {}
60 """Pretend we read the snap/manifest.yaml file"""
61 return io.StringIO(TEST_SNAP_MANIFEST_YAML)
6258
6359
64def _pkgfmt_type(self):60def _pkgfmt_type(self):
@@ -90,12 +86,9 @@ def create_patches():
90 patch("reviewtools.sr_common.SnapContainer.__init__", _BaseContainer.__init__)86 patch("reviewtools.sr_common.SnapContainer.__init__", _BaseContainer.__init__)
91 )87 )
92 patches.append(88 patches.append(
93 patch("reviewtools.sr_common.SnapReview._extract_snap_yaml", _extract_snap_yaml)
94 )
95 patches.append(
96 patch(89 patch(
97 "reviewtools.sr_common.SnapReview._extract_snap_manifest_yaml",90 "reviewtools.sr_common.SnapContainer.get_file_as_yaml",
98 _extract_snap_manifest_yaml,91 _BaseContainer.get_file_as_yaml,
99 )92 )
100 )93 )
101 # sr_common94 # sr_common
@@ -141,15 +134,11 @@ class TestSnapReview(TestCase):
141134
142 def _update_test_snap_yaml(self):135 def _update_test_snap_yaml(self):
143 global TEST_SNAP_YAML136 global TEST_SNAP_YAML
144 TEST_SNAP_YAML = yaml.dump(137 TEST_SNAP_YAML = self.test_snap_yaml
145 self.test_snap_yaml, default_flow_style=False, indent=4
146 )
147138
148 def _update_test_snap_manifest_yaml(self):139 def _update_test_snap_manifest_yaml(self):
149 global TEST_SNAP_MANIFEST_YAML140 global TEST_SNAP_MANIFEST_YAML
150 TEST_SNAP_MANIFEST_YAML = yaml.dump(141 TEST_SNAP_MANIFEST_YAML = self.test_snap_manifest_yaml
151 self.test_snap_manifest_yaml, default_flow_style=False, indent=4
152 )
153142
154 def _update_test_name(self):143 def _update_test_name(self):
155 self.test_name = "%s.origin_%s_%s.snap" % (144 self.test_name = "%s.origin_%s_%s.snap" % (
diff --git a/reviewtools/tests/containers/test_base_container.py b/reviewtools/tests/containers/test_base_container.py
index 1b4e022..277be8f 100644
--- a/reviewtools/tests/containers/test_base_container.py
+++ b/reviewtools/tests/containers/test_base_container.py
@@ -35,7 +35,15 @@ class TestBaseContainer(unittest.TestCase):
35 os.makedirs(os.path.dirname(unpacked_file), exist_ok=True)35 os.makedirs(os.path.dirname(unpacked_file), exist_ok=True)
36 if file not in self.expected_binaries:36 if file not in self.expected_binaries:
37 with open(unpacked_file, "w+b") as fd:37 with open(unpacked_file, "w+b") as fd:
38 fd.write(b"Test")38 if file == "meta/snap.yaml":
39 fd.write(
40 b"""
41map:
42 key: value
43 """
44 )
45 else:
46 fd.write(b"Test")
39 else:47 else:
40 shutil.copyfile(os.path.join("/", file), unpacked_file)48 shutil.copyfile(os.path.join("/", file), unpacked_file)
41 return unpack_dir49 return unpack_dir
@@ -126,10 +134,10 @@ class TestBaseContainer(unittest.TestCase):
126 try:134 try:
127 container = BaseContainer(self.fn)135 container = BaseContainer(self.fn)
128 container._unpack_dir = self.unpack_dir136 container._unpack_dir = self.unpack_dir
129 snap_yaml = container.get_file("meta/snap.yaml")137 snap_yaml = container.get_file("meta/icon.png")
130 self.assertEqual(snap_yaml.decode(), "Test")138 self.assertEqual(snap_yaml.decode(), "Test")
131 except ContainerException:139 except ContainerException:
132 self.fail("An unexpected error occurred while getting meta/snap.yaml")140 self.fail("An unexpected error occurred while getting meta/icon.png")
133141
134 def test_get_file__missing_file(self):142 def test_get_file__missing_file(self):
135 with self.assertRaises(ContainerException) as context:143 with self.assertRaises(ContainerException) as context:
@@ -138,6 +146,88 @@ class TestBaseContainer(unittest.TestCase):
138 container.get_file("non/existent/file")146 container.get_file("non/existent/file")
139 self.assertTrue("Could not find" in context.exception.value)147 self.assertTrue("Could not find" in context.exception.value)
140148
149 # Test get yaml
150 def test_get_file_as_yaml__happy(self):
151 try:
152 container = BaseContainer(self.fn)
153 container._unpack_dir = self.unpack_dir
154 snap_yaml = container.get_file_as_yaml("meta/snap.yaml")
155 self.assertEqual(snap_yaml, {"map": {"key": "value"}})
156 except ContainerException:
157 self.fail("An unexpected error occurred while getting meta/snap.yaml")
158
159 def test_get_file_as_yaml__dup_keys(self):
160 # Write the file to test
161 with open(os.path.join(self.unpack_dir, "test.yaml"), "w+b") as fd:
162 fd.write(
163 b"""
164map:
165 key: value1
166 key: value2
167 """
168 )
169
170 with self.assertRaises(ContainerException) as context:
171 container = BaseContainer(self.fn)
172 container._unpack_dir = self.unpack_dir
173 container.get_file_as_yaml("test.yaml")
174 self.assertTrue('found duplicate key "key"' in context.exception.value)
175
176 # Clean the tested file
177 os.unlink(os.path.join(self.unpack_dir, "test.yaml"))
178
179 def test_get_file_as_yaml__allow_dup_keys(self):
180 # Write the file to test
181 with open(os.path.join(self.unpack_dir, "test.yaml"), "w+b") as fd:
182 fd.write(
183 b"""
184map:
185 key: value1
186 key: value2
187 """
188 )
189
190 container = BaseContainer(self.fn)
191 container._unpack_dir = self.unpack_dir
192 container.get_file_as_yaml("test.yaml", allow_duplicate_keys=True)
193
194 # Clean the tested file
195 os.unlink(os.path.join(self.unpack_dir, "test.yaml"))
196
197 def test_get_file_as_yaml__version_1_1(self):
198 # Write the file to test
199 with open(os.path.join(self.unpack_dir, "test.yaml"), "w+b") as fd:
200 fd.write(
201 b"""
202mode: 0777
203 """
204 )
205
206 container = BaseContainer(self.fn)
207 container._unpack_dir = self.unpack_dir
208 test_yaml = container.get_file_as_yaml("test.yaml", yaml_version="1.1")
209 self.assertEqual(test_yaml["mode"], 511)
210
211 # Clean the tested file
212 os.unlink(os.path.join(self.unpack_dir, "test.yaml"))
213
214 def test_get_file_as_yaml__version_1_2(self):
215 # Write the file to test
216 with open(os.path.join(self.unpack_dir, "test.yaml"), "w+b") as fd:
217 fd.write(
218 b"""
219mode: 0777
220 """
221 )
222
223 container = BaseContainer(self.fn)
224 container._unpack_dir = self.unpack_dir
225 test_yaml = container.get_file_as_yaml("test.yaml")
226 self.assertEqual(test_yaml["mode"], 777)
227
228 # Clean the tested file
229 os.unlink(os.path.join(self.unpack_dir, "test.yaml"))
230
141 # Test get files list231 # Test get files list
142 def test_get_files_list__happy(self):232 def test_get_files_list__happy(self):
143 try:233 try:
diff --git a/reviewtools/tests/test_bbb_example_sr_skeleton.py b/reviewtools/tests/test_bbb_example_sr_skeleton.py
index 5a304cd..0abf94e 100644
--- a/reviewtools/tests/test_bbb_example_sr_skeleton.py
+++ b/reviewtools/tests/test_bbb_example_sr_skeleton.py
@@ -63,7 +63,6 @@ class TestSnapReviewSkeleton(sr_tests.TestSnapReview):
63 self._update_test_name()63 self._update_test_name()
6464
65 import pprint65 import pprint
66 import yaml
6766
68 print(67 print(
69 """68 """
@@ -75,4 +74,4 @@ class TestSnapReviewSkeleton(sr_tests.TestSnapReview):
75"""74"""
76 % (self.test_name)75 % (self.test_name)
77 )76 )
78 pprint.pprint(yaml.load(sr_tests.TEST_SNAP_YAML, Loader=yaml.SafeLoader))77 pprint.pprint(sr_tests.TEST_SNAP_YAML)
diff --git a/reviewtools/tests/test_sr_common.py b/reviewtools/tests/test_sr_common.py
79deleted file mode 10064478deleted file mode 100644
index 541cea9..0000000
--- a/reviewtools/tests/test_sr_common.py
+++ /dev/null
@@ -1,38 +0,0 @@
1"""test_sr_common.py: tests for the sr_common module"""
2#
3# Copyright (C) 2018 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; version 3 of the License.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17from reviewtools.sr_common import SnapReview
18import reviewtools.sr_tests as sr_tests
19
20
21class TestSnapReviewCommon(sr_tests.TestSnapReview):
22 def setUp(self):
23 super().setUp()
24 self.review = SnapReview("app.snap", "sr_common_review_type")
25
26 def test_no_duplicated_yaml_keys(self):
27 """Check _no_duplicated_yaml_keys"""
28 b1 = """
29map:
30 key:
31 key:
32 """
33 for ok in [
34 b1,
35 ]:
36 with self.assertRaises(Exception) as e:
37 self.review._verify_no_duplicated_yaml_keys(ok)
38 self.assertRegex(str(e.exception), r'found duplicate key "key".*')

Subscribers

People subscribed via source and target branches