1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3# You can obtain one at http://mozilla.org/MPL/2.0/.
4from __future__ import absolute_import
5from __future__ import unicode_literals
6
7import sys
8import unittest
9
10from mozfile.mozfile import NamedTemporaryFile
11
12from mach.config import (
13    BooleanType,
14    ConfigException,
15    ConfigSettings,
16    IntegerType,
17    PathType,
18    PositiveIntegerType,
19    StringType,
20)
21from mach.decorators import SettingsProvider
22from mozunit import main
23from six import string_types
24
25
26CONFIG1 = r"""
27[foo]
28
29bar = bar_value
30baz = /baz/foo.c
31"""
32
33CONFIG2 = r"""
34[foo]
35
36bar = value2
37"""
38
39
40@SettingsProvider
41class Provider1(object):
42    config_settings = [
43        ("foo.bar", StringType, "desc"),
44        ("foo.baz", PathType, "desc"),
45    ]
46
47
48@SettingsProvider
49class ProviderDuplicate(object):
50    config_settings = [
51        ("dupesect.foo", StringType, "desc"),
52        ("dupesect.foo", StringType, "desc"),
53    ]
54
55
56@SettingsProvider
57class Provider2(object):
58    config_settings = [
59        ("a.string", StringType, "desc"),
60        ("a.boolean", BooleanType, "desc"),
61        ("a.pos_int", PositiveIntegerType, "desc"),
62        ("a.int", IntegerType, "desc"),
63        ("a.path", PathType, "desc"),
64    ]
65
66
67@SettingsProvider
68class Provider3(object):
69    @classmethod
70    def config_settings(cls):
71        return [
72            ("a.string", "string", "desc"),
73            ("a.boolean", "boolean", "desc"),
74            ("a.pos_int", "pos_int", "desc"),
75            ("a.int", "int", "desc"),
76            ("a.path", "path", "desc"),
77        ]
78
79
80@SettingsProvider
81class Provider4(object):
82    config_settings = [
83        ("foo.abc", StringType, "desc", "a", {"choices": set("abc")}),
84        ("foo.xyz", StringType, "desc", "w", {"choices": set("xyz")}),
85    ]
86
87
88@SettingsProvider
89class Provider5(object):
90    config_settings = [
91        ("foo.*", "string", "desc"),
92        ("foo.bar", "string", "desc"),
93    ]
94
95
96class TestConfigSettings(unittest.TestCase):
97    def test_empty(self):
98        s = ConfigSettings()
99
100        self.assertEqual(len(s), 0)
101        self.assertNotIn("foo", s)
102
103    def test_duplicate_option(self):
104        s = ConfigSettings()
105
106        with self.assertRaises(ConfigException):
107            s.register_provider(ProviderDuplicate)
108
109    def test_simple(self):
110        s = ConfigSettings()
111        s.register_provider(Provider1)
112
113        self.assertEqual(len(s), 1)
114        self.assertIn("foo", s)
115
116        foo = s["foo"]
117        foo = s.foo
118
119        self.assertEqual(len(foo), 0)
120        self.assertEqual(len(foo._settings), 2)
121
122        self.assertIn("bar", foo._settings)
123        self.assertIn("baz", foo._settings)
124
125        self.assertNotIn("bar", foo)
126        foo["bar"] = "value1"
127        self.assertIn("bar", foo)
128
129        self.assertEqual(foo["bar"], "value1")
130        self.assertEqual(foo.bar, "value1")
131
132    def test_assignment_validation(self):
133        s = ConfigSettings()
134        s.register_provider(Provider2)
135
136        a = s.a
137
138        # Assigning an undeclared setting raises.
139        exc_type = AttributeError if sys.version_info < (3, 0) else KeyError
140        with self.assertRaises(exc_type):
141            a.undefined = True
142
143        with self.assertRaises(KeyError):
144            a["undefined"] = True
145
146        # Basic type validation.
147        a.string = "foo"
148        a.string = "foo"
149
150        with self.assertRaises(TypeError):
151            a.string = False
152
153        a.boolean = True
154        a.boolean = False
155
156        with self.assertRaises(TypeError):
157            a.boolean = "foo"
158
159        a.pos_int = 5
160        a.pos_int = 0
161
162        with self.assertRaises(ValueError):
163            a.pos_int = -1
164
165        with self.assertRaises(TypeError):
166            a.pos_int = "foo"
167
168        a.int = 5
169        a.int = 0
170        a.int = -5
171
172        with self.assertRaises(TypeError):
173            a.int = 1.24
174
175        with self.assertRaises(TypeError):
176            a.int = "foo"
177
178        a.path = "/home/gps"
179        a.path = "foo.c"
180        a.path = "foo/bar"
181        a.path = "./foo"
182
183    def retrieval_type_helper(self, provider):
184        s = ConfigSettings()
185        s.register_provider(provider)
186
187        a = s.a
188
189        a.string = "foo"
190        a.boolean = True
191        a.pos_int = 12
192        a.int = -4
193        a.path = "./foo/bar"
194
195        self.assertIsInstance(a.string, string_types)
196        self.assertIsInstance(a.boolean, bool)
197        self.assertIsInstance(a.pos_int, int)
198        self.assertIsInstance(a.int, int)
199        self.assertIsInstance(a.path, string_types)
200
201    def test_retrieval_type(self):
202        self.retrieval_type_helper(Provider2)
203        self.retrieval_type_helper(Provider3)
204
205    def test_choices_validation(self):
206        s = ConfigSettings()
207        s.register_provider(Provider4)
208
209        foo = s.foo
210        foo.abc
211        with self.assertRaises(ValueError):
212            foo.xyz
213
214        with self.assertRaises(ValueError):
215            foo.abc = "e"
216
217        foo.abc = "b"
218        foo.xyz = "y"
219
220    def test_wildcard_options(self):
221        s = ConfigSettings()
222        s.register_provider(Provider5)
223
224        foo = s.foo
225
226        self.assertIn("*", foo._settings)
227        self.assertNotIn("*", foo)
228
229        foo.baz = "value1"
230        foo.bar = "value2"
231
232        self.assertIn("baz", foo)
233        self.assertEqual(foo.baz, "value1")
234
235        self.assertIn("bar", foo)
236        self.assertEqual(foo.bar, "value2")
237
238    def test_file_reading_single(self):
239        temp = NamedTemporaryFile(mode="wt")
240        temp.write(CONFIG1)
241        temp.flush()
242
243        s = ConfigSettings()
244        s.register_provider(Provider1)
245
246        s.load_file(temp.name)
247
248        self.assertEqual(s.foo.bar, "bar_value")
249
250    def test_file_reading_multiple(self):
251        """Loading multiple files has proper overwrite behavior."""
252        temp1 = NamedTemporaryFile(mode="wt")
253        temp1.write(CONFIG1)
254        temp1.flush()
255
256        temp2 = NamedTemporaryFile(mode="wt")
257        temp2.write(CONFIG2)
258        temp2.flush()
259
260        s = ConfigSettings()
261        s.register_provider(Provider1)
262
263        s.load_files([temp1.name, temp2.name])
264
265        self.assertEqual(s.foo.bar, "value2")
266
267    def test_file_reading_missing(self):
268        """Missing files should silently be ignored."""
269
270        s = ConfigSettings()
271
272        s.load_file("/tmp/foo.ini")
273
274    def test_file_writing(self):
275        s = ConfigSettings()
276        s.register_provider(Provider2)
277
278        s.a.string = "foo"
279        s.a.boolean = False
280
281        temp = NamedTemporaryFile("wt")
282        s.write(temp)
283        temp.flush()
284
285        s2 = ConfigSettings()
286        s2.register_provider(Provider2)
287
288        s2.load_file(temp.name)
289
290        self.assertEqual(s.a.string, s2.a.string)
291        self.assertEqual(s.a.boolean, s2.a.boolean)
292
293
294if __name__ == "__main__":
295    main()
296