Merge ~kissiel/checkbox-support:snap_utils into checkbox-support:master

Proposed by Maciej Kisielewski
Status: Merged
Approved by: Maciej Kisielewski
Approved revision: d4d3379cbb78eca9a2e80b09e44d11ab39026f2d
Merged at revision: e26b2edf4e91b7ddf67ccb5417a129ccf3e91740
Proposed branch: ~kissiel/checkbox-support:snap_utils
Merge into: checkbox-support:master
Diff against target: 282 lines (+253/-1)
2 files modified
checkbox_support/snap_utils/config.py (+35/-1)
checkbox_support/snap_utils/tests/test_config.py (+218/-0)
Reviewer Review Type Date Requested Status
Jonathan Cave (community) Approve
Sheila Miguez (community) Approve
Maciej Kisielewski Needs Resubmitting
Review via email: mp+332121@code.launchpad.net

Description of the change

Add top level functions to manipulate configs and a bunch of tests

To post a comment you must log in.
Revision history for this message
Sheila Miguez (codersquid) wrote :

I have a minor minor inline comment about a docstring.

Revision history for this message
Maciej Kisielewski (kissiel) wrote :

> I have a minor minor inline comment about a docstring.

Not only that check is welcome, by introducing it I found that my logic was wrong. TYVM!

Fixed, squashed, pushed.

review: Needs Resubmitting
Revision history for this message
Jonathan Cave (jocave) wrote :

Couple of questions inline..

review: Needs Information
Revision history for this message
Sheila Miguez (codersquid) wrote :

very happy to help.

review: Approve
Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Answers inline.

Revision history for this message
Jonathan Cave (jocave) wrote :

Thanks

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/checkbox_support/snap_utils/config.py b/checkbox_support/snap_utils/config.py
2index d5aac7d..eb2b1aa 100644
3--- a/checkbox_support/snap_utils/config.py
4+++ b/checkbox_support/snap_utils/config.py
5@@ -92,7 +92,7 @@ def get_configuration_set():
6 continue
7 k, _, v = line.partition('=')
8 if not key_re.match(k):
9- raise SystemExit("%s is not a valid configuration key" % k)
10+ raise ValueError("%s is not a valid configuration key" % k)
11 # snapd accepts lowercase and dashes only for config names
12 # so let's "mangle" the names to match the requirement
13 k = k.replace('_', '-').lower()
14@@ -102,6 +102,7 @@ def get_configuration_set():
15 pass
16 return config_set
17
18+
19 def write_checkbox_conf(configuration):
20 """Write checkbox.conf in $SNAP_DATA dir."""
21 config = configparser.ConfigParser()
22@@ -117,3 +118,36 @@ def write_checkbox_conf(configuration):
23 os.makedirs(os.path.dirname(checkbox_conf_path), exist_ok=True)
24 with open(checkbox_conf_path, 'wt') as stream:
25 config.write(stream)
26+
27+
28+def refresh_configuration():
29+ """
30+ Read config_vars, write the ones missing in snapd
31+ and call update configuration in both, snapd and checkbox.conf.
32+ """
33+ conf_set = get_configuration_set()
34+ if conf_set:
35+ current = get_snapctl_config(list(conf_set.keys()))
36+ for key in conf_set.keys():
37+ if key not in current.keys() or not current[key]:
38+ current[key] = conf_set[key]
39+ update_configuration(current)
40+
41+
42+def update_configuration(updated_entries):
43+ """
44+ Update snapd configuration and write checkbox.conf file
45+
46+ :param updated_entries:
47+ A dict containing the configuration to set.
48+ Keys should contain lowercase letters, dashes and number only.
49+ """
50+ vars_to_set = []
51+ key_re = re.compile(r"^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$")
52+ for k, v in updated_entries.items():
53+ if not key_re.match(k):
54+ raise ValueError("'%s' is not a valid key" % k)
55+ vars_to_set.append('{}={}'.format(k.replace('_', '-').lower(), v))
56+ subprocess.run(['snapctl', 'set'] + sorted(vars_to_set))
57+ write_checkbox_conf(
58+ get_snapctl_config(list(get_configuration_set().keys())))
59diff --git a/checkbox_support/snap_utils/tests/test_config.py b/checkbox_support/snap_utils/tests/test_config.py
60new file mode 100644
61index 0000000..74064c4
62--- /dev/null
63+++ b/checkbox_support/snap_utils/tests/test_config.py
64@@ -0,0 +1,218 @@
65+# Copyright 2017 Canonical Ltd.
66+# All rights reserved.
67+#
68+# Written by:
69+# Maciej Kisielewski <maciej.kisielewski@canonical.com>
70+
71+import unittest
72+import sys
73+
74+from textwrap import dedent
75+from unittest.mock import mock_open, patch
76+
77+from checkbox_support.snap_utils.config import get_configuration_set
78+from checkbox_support.snap_utils.config import get_snapctl_config
79+from checkbox_support.snap_utils.config import refresh_configuration
80+from checkbox_support.snap_utils.config import write_checkbox_conf
81+
82+
83+class TestSnapctlConfig(unittest.TestCase):
84+ def test_no_keys(self):
85+ self.assertEqual(get_snapctl_config([]), dict())
86+
87+ def test_one_key(self):
88+ with patch('subprocess.check_output', return_value=b'bar\n') as p:
89+ expected = {'foo': 'bar'}
90+ self.assertEqual(get_snapctl_config(['foo']), expected)
91+ p.assert_called_with(['snapctl', 'get', 'foo'])
92+
93+ def test_not_set_key(self):
94+ with patch('subprocess.check_output', return_value=b'\n') as p:
95+ expected = {'foo': ''}
96+ self.assertEqual(get_snapctl_config(['foo']), expected)
97+ p.assert_called_with(['snapctl', 'get', 'foo'])
98+
99+ def test_two_keys(self):
100+ SNAPCTL_OUT = dedent("""
101+ {
102+ \t"foo": "bar",
103+ \t"biz": "baz"
104+ }
105+ """).lstrip().encode(sys.stdout.encoding)
106+ with patch('subprocess.check_output', return_value=SNAPCTL_OUT) as p:
107+ expected = {'foo': 'bar', 'biz': 'baz'}
108+ self.assertEqual(get_snapctl_config(['foo', 'biz']), expected)
109+ p.assert_called_with(['snapctl', 'get', 'foo', 'biz'])
110+
111+ def test_two_keys_one_missing(self):
112+ SNAPCTL_OUT = dedent("""
113+ {
114+ \t"foo": "bar"
115+ }
116+ """).lstrip().encode(sys.stdout.encoding)
117+ with patch('subprocess.check_output', return_value=SNAPCTL_OUT) as p:
118+ expected = {'foo': 'bar'}
119+ self.assertEqual(get_snapctl_config(['foo', 'biz']), expected)
120+ p.assert_called_with(['snapctl', 'get', 'foo', 'biz'])
121+
122+ def test_two_keys_both_missing(self):
123+ with patch('subprocess.check_output', return_value=b'{}\n') as p:
124+ self.assertEqual(get_snapctl_config(['foo', 'biz']), dict())
125+ p.assert_called_with(['snapctl', 'get', 'foo', 'biz'])
126+
127+
128+class TestConfigSet(unittest.TestCase):
129+ def test_empty_on_missing(self):
130+ class FNFE_raiser():
131+ def __call__(self, *args):
132+ raise FileNotFoundError()
133+ with patch('builtins.open', new_callable=FNFE_raiser, create=True):
134+ result = get_configuration_set()
135+ self.assertEqual(result, dict())
136+
137+ def test_correct_values(self):
138+ with patch('builtins.open', mock_open(read_data='FOO=bar\nBAZ=Biz')):
139+ result = get_configuration_set()
140+ self.assertEqual(result, {'foo': 'bar', 'baz': 'Biz'})
141+
142+ def test_comments_ignored(self):
143+ DATA = """
144+ # comment
145+ FOO=bar
146+ # indented comment
147+ BAZ=Biz
148+ """
149+ with patch('builtins.open', mock_open(read_data=DATA)):
150+ result = get_configuration_set()
151+ self.assertEqual(result, {'foo': 'bar', 'baz': 'Biz'})
152+
153+ def test_inline_comments_is_val(self):
154+ DATA = 'FOO=bar # inline comment'
155+ with patch('builtins.open', mock_open(read_data=DATA)):
156+ result = get_configuration_set()
157+ self.assertEqual(result, {'foo': 'bar # inline comment'})
158+
159+ def test_lowercase_key_raises(self):
160+ DATA = 'foo=bar'
161+ with patch('builtins.open', mock_open(read_data=DATA)):
162+ expected_msg = 'foo is not a valid configuration key'
163+ with self.assertRaisesRegex(ValueError, expected_msg):
164+ get_configuration_set()
165+
166+ def test_empty_on_empty_file(self):
167+ with patch('builtins.open', mock_open(read_data='')):
168+ self.assertEqual(get_configuration_set(), dict())
169+
170+
171+class TestWriteCheckboxConf(unittest.TestCase):
172+ def test_smoke(self):
173+ m = mock_open()
174+ with patch('builtins.open', m):
175+ write_checkbox_conf({'foo': 'bar'})
176+ m().write.called_once_with('[environ]\n')
177+ m().write.called_once_with('FOO = bar\n')
178+ m().write.called_once_with('\n')
179+ self.assertEqual(m().write.call_count, 3)
180+
181+ def test_writes_empty(self):
182+ m = mock_open()
183+ with patch('builtins.open', m):
184+ write_checkbox_conf({})
185+ m().write.called_once_with('[environ]\n')
186+ m().write.called_once_with('\n')
187+ self.assertEqual(m().write.call_count, 2)
188+
189+
190+class ConfigIntegrationTests(unittest.TestCase):
191+ """
192+ Integration tests for configuration manipulation.
193+
194+ The test have following structure:
195+ establish values in config_vars
196+ establish values in snapd database
197+ expect calls to `snapctl set`
198+ expect checkbox.conf to be written
199+ """
200+ @patch('checkbox_support.snap_utils.config.get_configuration_set')
201+ @patch('checkbox_support.snap_utils.config.write_checkbox_conf')
202+ @patch('subprocess.run')
203+ def test_empty_on_missing(self, mock_run, mock_write, mock_conf_set):
204+ """ nothing in config_vars,
205+ nothing in snapd db,
206+ so checkbox.conf should not be written
207+ and no calls to snapctl should be made
208+ """
209+ mock_conf_set.return_value = {}
210+ refresh_configuration()
211+ self.assertTrue(mock_conf_set.called)
212+ self.assertFalse(mock_write.called)
213+ self.assertFalse(mock_run.called)
214+
215+ @patch('checkbox_support.snap_utils.config.get_configuration_set')
216+ @patch('subprocess.check_output', return_value=b'\n')
217+ @patch('subprocess.run')
218+ def test_one_value(self, mock_run, mock_subproc, mock_conf_set):
219+ """ FOO=bar in config_vars,
220+ nothing in snapd db,
221+ so checkbox.conf should be written with:
222+ "FOO=bar"
223+ and snapctl should be called with:
224+ foo=bar
225+ """
226+ mock_conf_set.return_value = {'foo': 'bar'}
227+ m = mock_open()
228+ with patch('builtins.open', m):
229+ refresh_configuration()
230+ m().write.called_once_with('[environ]\n')
231+ m().write.called_once_with('FOO = bar\n')
232+ m().write.called_once_with('\n')
233+ self.assertTrue(mock_conf_set.called)
234+ mock_run.assert_called_once_with(['snapctl', 'set', 'foo=bar'])
235+
236+ @patch('checkbox_support.snap_utils.config.get_configuration_set')
237+ @patch('subprocess.check_output')
238+ @patch('subprocess.run')
239+ def test_one_value_overriden_by_config(
240+ self, mock_run, mock_subproc, mock_conf_set):
241+ """
242+ FOO=default in config_vars,
243+ and snapd db has foo=bar,
244+ so checkbox.conf should be written with:
245+ "FOO=bar"
246+ """
247+ mock_conf_set.return_value = {'foo': 'default'}
248+ mock_subproc.return_value = b'bar\n'
249+ m = mock_open()
250+ with patch('builtins.open', m):
251+ refresh_configuration()
252+ m().write.called_once_with('[environ]\n')
253+ m().write.called_once_with('FOO = bar\n')
254+ m().write.called_once_with('\n')
255+ mock_run.assert_called_once_with(['snapctl', 'set', 'foo=bar'])
256+
257+ @patch('checkbox_support.snap_utils.config.get_configuration_set')
258+ @patch('subprocess.check_output')
259+ @patch('subprocess.run')
260+ def test_one_new_one_existing(
261+ self, mock_run, mock_subproc, mock_conf_set):
262+ """
263+ FOO=bar BIZ=baz in config_vars,
264+ and snapd db has foo=old,
265+ so checkbox.conf should be written with:
266+ "FOO=old and BIZ=baz"
267+ """
268+ mock_conf_set.return_value = {'foo': 'bar', 'biz': 'baz'}
269+ mock_subproc.return_value = dedent("""
270+ {
271+ \t"foo": "old"
272+ }
273+ """).lstrip().encode(sys.stdout.encoding)
274+ m = mock_open()
275+ with patch('builtins.open', m):
276+ refresh_configuration()
277+ m().write.called_once_with('[environ]\n')
278+ m().write.called_once_with('FOO = old\n')
279+ m().write.called_once_with('BIZ = baz\n')
280+ m().write.called_once_with('\n')
281+ mock_run.assert_called_once_with(
282+ ['snapctl', 'set', 'biz=baz', 'foo=old'])

Subscribers

People subscribed via source and target branches