1 | """ |
---|
2 | Tests for allmydata.util.configutil. |
---|
3 | |
---|
4 | Ported to Python 3. |
---|
5 | """ |
---|
6 | |
---|
7 | import os.path |
---|
8 | from configparser import ( |
---|
9 | ConfigParser, |
---|
10 | ) |
---|
11 | from functools import ( |
---|
12 | partial, |
---|
13 | ) |
---|
14 | |
---|
15 | from hypothesis import ( |
---|
16 | given, |
---|
17 | ) |
---|
18 | from hypothesis.strategies import ( |
---|
19 | dictionaries, |
---|
20 | text, |
---|
21 | characters, |
---|
22 | ) |
---|
23 | |
---|
24 | from twisted.python.filepath import ( |
---|
25 | FilePath, |
---|
26 | ) |
---|
27 | from twisted.trial import unittest |
---|
28 | |
---|
29 | from allmydata.util import configutil |
---|
30 | |
---|
31 | |
---|
32 | def arbitrary_config_dicts( |
---|
33 | min_sections=0, |
---|
34 | max_sections=3, |
---|
35 | max_section_name_size=8, |
---|
36 | max_items_per_section=3, |
---|
37 | max_item_length=8, |
---|
38 | max_value_length=8, |
---|
39 | ): |
---|
40 | """ |
---|
41 | Build ``dict[str, dict[str, str]]`` instances populated with arbitrary |
---|
42 | configurations. |
---|
43 | """ |
---|
44 | identifier_text = partial( |
---|
45 | text, |
---|
46 | # Don't allow most control characters or spaces |
---|
47 | alphabet=characters( |
---|
48 | blacklist_categories=('Cc', 'Cs', 'Zs'), |
---|
49 | ), |
---|
50 | ) |
---|
51 | return dictionaries( |
---|
52 | identifier_text( |
---|
53 | min_size=1, |
---|
54 | max_size=max_section_name_size, |
---|
55 | ), |
---|
56 | dictionaries( |
---|
57 | identifier_text( |
---|
58 | min_size=1, |
---|
59 | max_size=max_item_length, |
---|
60 | ), |
---|
61 | text(max_size=max_value_length), |
---|
62 | max_size=max_items_per_section, |
---|
63 | ), |
---|
64 | min_size=min_sections, |
---|
65 | max_size=max_sections, |
---|
66 | ) |
---|
67 | |
---|
68 | |
---|
69 | def to_configparser(dictconfig): |
---|
70 | """ |
---|
71 | Take a ``dict[str, dict[str, str]]`` and turn it into the corresponding |
---|
72 | populated ``ConfigParser`` instance. |
---|
73 | """ |
---|
74 | cp = ConfigParser() |
---|
75 | for section, items in dictconfig.items(): |
---|
76 | cp.add_section(section) |
---|
77 | for k, v in items.items(): |
---|
78 | cp.set( |
---|
79 | section, |
---|
80 | k, |
---|
81 | # ConfigParser has a feature that everyone knows and loves |
---|
82 | # where it will use %-style interpolation to substitute |
---|
83 | # values from one part of the config into another part of |
---|
84 | # the config. Escape all our `%`s to avoid hitting this |
---|
85 | # and complicating things. |
---|
86 | v.replace("%", "%%"), |
---|
87 | ) |
---|
88 | return cp |
---|
89 | |
---|
90 | |
---|
91 | class ConfigUtilTests(unittest.TestCase): |
---|
92 | def setUp(self): |
---|
93 | super(ConfigUtilTests, self).setUp() |
---|
94 | self.static_valid_config = configutil.ValidConfiguration( |
---|
95 | dict(node=['valid']), |
---|
96 | ) |
---|
97 | self.dynamic_valid_config = configutil.ValidConfiguration( |
---|
98 | dict(), |
---|
99 | lambda section_name: section_name == "node", |
---|
100 | lambda section_name, item_name: (section_name, item_name) == ("node", "valid"), |
---|
101 | ) |
---|
102 | |
---|
103 | def create_tahoe_cfg(self, cfg): |
---|
104 | d = self.mktemp() |
---|
105 | os.mkdir(d) |
---|
106 | fname = os.path.join(d, 'tahoe.cfg') |
---|
107 | with open(fname, "w") as f: |
---|
108 | f.write(cfg) |
---|
109 | return fname |
---|
110 | |
---|
111 | def test_config_utils(self): |
---|
112 | tahoe_cfg = self.create_tahoe_cfg("""\ |
---|
113 | [node] |
---|
114 | nickname = client-0 |
---|
115 | web.port = adopt-socket:fd=5 |
---|
116 | [storage] |
---|
117 | enabled = false |
---|
118 | """) |
---|
119 | |
---|
120 | # test that at least one option was read correctly |
---|
121 | config = configutil.get_config(tahoe_cfg) |
---|
122 | self.failUnlessEqual(config.get("node", "nickname"), "client-0") |
---|
123 | |
---|
124 | # test that set_config can mutate an existing option |
---|
125 | configutil.set_config(config, "node", "nickname", "Alice!") |
---|
126 | configutil.write_config(FilePath(tahoe_cfg), config) |
---|
127 | |
---|
128 | config = configutil.get_config(tahoe_cfg) |
---|
129 | self.failUnlessEqual(config.get("node", "nickname"), "Alice!") |
---|
130 | |
---|
131 | # test that set_config can set a new option |
---|
132 | descriptor = "Twas brillig, and the slithy toves Did gyre and gimble in the wabe" |
---|
133 | configutil.set_config(config, "node", "descriptor", descriptor) |
---|
134 | configutil.write_config(FilePath(tahoe_cfg), config) |
---|
135 | |
---|
136 | config = configutil.get_config(tahoe_cfg) |
---|
137 | self.failUnlessEqual(config.get("node", "descriptor"), descriptor) |
---|
138 | |
---|
139 | def test_config_validation_success(self): |
---|
140 | """ |
---|
141 | ``configutil.validate_config`` returns ``None`` when the configuration it |
---|
142 | is given has nothing more than the static sections and items defined |
---|
143 | by the validator. |
---|
144 | """ |
---|
145 | # should succeed, no exceptions |
---|
146 | configutil.validate_config( |
---|
147 | "<test_config_validation_success>", |
---|
148 | to_configparser({"node": {"valid": "foo"}}), |
---|
149 | self.static_valid_config, |
---|
150 | ) |
---|
151 | |
---|
152 | def test_config_dynamic_validation_success(self): |
---|
153 | """ |
---|
154 | A configuration with sections and items that are not matched by the static |
---|
155 | validation but are matched by the dynamic validation is considered |
---|
156 | valid. |
---|
157 | """ |
---|
158 | # should succeed, no exceptions |
---|
159 | configutil.validate_config( |
---|
160 | "<test_config_dynamic_validation_success>", |
---|
161 | to_configparser({"node": {"valid": "foo"}}), |
---|
162 | self.dynamic_valid_config, |
---|
163 | ) |
---|
164 | |
---|
165 | def test_config_validation_invalid_item(self): |
---|
166 | config = to_configparser({"node": {"valid": "foo", "invalid": "foo"}}) |
---|
167 | e = self.assertRaises( |
---|
168 | configutil.UnknownConfigError, |
---|
169 | configutil.validate_config, |
---|
170 | "<test_config_validation_invalid_item>", |
---|
171 | config, |
---|
172 | self.static_valid_config, |
---|
173 | ) |
---|
174 | self.assertIn("section [node] contains unknown option 'invalid'", str(e)) |
---|
175 | |
---|
176 | def test_config_validation_invalid_section(self): |
---|
177 | """ |
---|
178 | A configuration with a section that is matched by neither the static nor |
---|
179 | dynamic validators is rejected. |
---|
180 | """ |
---|
181 | config = to_configparser({"node": {"valid": "foo"}, "invalid": {}}) |
---|
182 | e = self.assertRaises( |
---|
183 | configutil.UnknownConfigError, |
---|
184 | configutil.validate_config, |
---|
185 | "<test_config_validation_invalid_section>", |
---|
186 | config, |
---|
187 | self.static_valid_config, |
---|
188 | ) |
---|
189 | self.assertIn("contains unknown section [invalid]", str(e)) |
---|
190 | |
---|
191 | def test_config_dynamic_validation_invalid_section(self): |
---|
192 | """ |
---|
193 | A configuration with a section that is matched by neither the static nor |
---|
194 | dynamic validators is rejected. |
---|
195 | """ |
---|
196 | config = to_configparser({"node": {"valid": "foo"}, "invalid": {}}) |
---|
197 | e = self.assertRaises( |
---|
198 | configutil.UnknownConfigError, |
---|
199 | configutil.validate_config, |
---|
200 | "<test_config_dynamic_validation_invalid_section>", |
---|
201 | config, |
---|
202 | self.dynamic_valid_config, |
---|
203 | ) |
---|
204 | self.assertIn("contains unknown section [invalid]", str(e)) |
---|
205 | |
---|
206 | def test_config_dynamic_validation_invalid_item(self): |
---|
207 | """ |
---|
208 | A configuration with a section, item pair that is matched by neither the |
---|
209 | static nor dynamic validators is rejected. |
---|
210 | """ |
---|
211 | config = to_configparser({"node": {"valid": "foo", "invalid": "foo"}}) |
---|
212 | e = self.assertRaises( |
---|
213 | configutil.UnknownConfigError, |
---|
214 | configutil.validate_config, |
---|
215 | "<test_config_dynamic_validation_invalid_item>", |
---|
216 | config, |
---|
217 | self.dynamic_valid_config, |
---|
218 | ) |
---|
219 | self.assertIn("section [node] contains unknown option 'invalid'", str(e)) |
---|
220 | |
---|
221 | def test_duplicate_sections(self): |
---|
222 | """ |
---|
223 | Duplicate section names are merged. |
---|
224 | """ |
---|
225 | fname = self.create_tahoe_cfg('[node]\na = foo\n[node]\n b = bar\n') |
---|
226 | config = configutil.get_config(fname) |
---|
227 | self.assertEqual(config.get("node", "a"), "foo") |
---|
228 | self.assertEqual(config.get("node", "b"), "bar") |
---|
229 | |
---|
230 | @given(arbitrary_config_dicts()) |
---|
231 | def test_everything_valid(self, cfgdict): |
---|
232 | """ |
---|
233 | ``validate_config`` returns ``None`` when the validator is |
---|
234 | ``ValidConfiguration.everything()``. |
---|
235 | """ |
---|
236 | cfg = to_configparser(cfgdict) |
---|
237 | self.assertIs( |
---|
238 | configutil.validate_config( |
---|
239 | "<test_everything_valid>", |
---|
240 | cfg, |
---|
241 | configutil.ValidConfiguration.everything(), |
---|
242 | ), |
---|
243 | None, |
---|
244 | ) |
---|
245 | |
---|
246 | @given(arbitrary_config_dicts(min_sections=1)) |
---|
247 | def test_nothing_valid(self, cfgdict): |
---|
248 | """ |
---|
249 | ``validate_config`` raises ``UnknownConfigError`` when the validator is |
---|
250 | ``ValidConfiguration.nothing()`` for all non-empty configurations. |
---|
251 | """ |
---|
252 | cfg = to_configparser(cfgdict) |
---|
253 | with self.assertRaises(configutil.UnknownConfigError): |
---|
254 | configutil.validate_config( |
---|
255 | "<test_everything_valid>", |
---|
256 | cfg, |
---|
257 | configutil.ValidConfiguration.nothing(), |
---|
258 | ) |
---|
259 | |
---|
260 | def test_nothing_empty_valid(self): |
---|
261 | """ |
---|
262 | ``validate_config`` returns ``None`` when the validator is |
---|
263 | ``ValidConfiguration.nothing()`` if the configuration is empty. |
---|
264 | """ |
---|
265 | cfg = ConfigParser() |
---|
266 | self.assertIs( |
---|
267 | configutil.validate_config( |
---|
268 | "<test_everything_valid>", |
---|
269 | cfg, |
---|
270 | configutil.ValidConfiguration.nothing(), |
---|
271 | ), |
---|
272 | None, |
---|
273 | ) |
---|
274 | |
---|
275 | @given(arbitrary_config_dicts()) |
---|
276 | def test_copy_config(self, cfgdict): |
---|
277 | """ |
---|
278 | ``copy_config`` creates a new ``ConfigParser`` object containing the same |
---|
279 | values as its input. |
---|
280 | """ |
---|
281 | cfg = to_configparser(cfgdict) |
---|
282 | copied = configutil.copy_config(cfg) |
---|
283 | # Should be equal |
---|
284 | self.assertEqual(cfg, copied) |
---|
285 | # But not because they're the same object. |
---|
286 | self.assertIsNot(cfg, copied) |
---|