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

Subscribers

People subscribed via source and target branches