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
1diff --git a/reviewtools/containers/base_container.py b/reviewtools/containers/base_container.py
2index 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.
70diff --git a/reviewtools/sr_common.py b/reviewtools/sr_common.py
71index 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)
172diff --git a/reviewtools/sr_tests.py b/reviewtools/sr_tests.py
173index 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" % (
247diff --git a/reviewtools/tests/containers/test_base_container.py b/reviewtools/tests/containers/test_base_container.py
248index 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:
370diff --git a/reviewtools/tests/test_bbb_example_sr_skeleton.py b/reviewtools/tests/test_bbb_example_sr_skeleton.py
371index 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)
388diff --git a/reviewtools/tests/test_sr_common.py b/reviewtools/tests/test_sr_common.py
389deleted file mode 100644
390index 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".*')

Subscribers

People subscribed via source and target branches