xref: /aosp_15_r20/external/pigweed/pw_ide/py/editors_test.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1# Copyright 2022 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Tests for pw_ide.editors"""
15
16from collections import OrderedDict
17from enum import Enum
18import unittest
19
20from pw_ide.editors import (
21    dict_deep_merge,
22    dict_swap_type,
23    EditorSettingsFile,
24    EditorSettingsManager,
25    JsonFileFormat,
26    Json5FileFormat,
27    YamlFileFormat,
28    _StructuredFileFormat,
29)
30
31from test_cases import PwIdeTestCase
32
33
34class TestDictDeepMerge(unittest.TestCase):
35    """Tests dict_deep_merge"""
36
37    # pylint: disable=unnecessary-lambda
38    def test_invariants_with_dict_success(self):
39        dict_a = {'hello': 'world'}
40        dict_b = {'foo': 'bar'}
41
42        expected = {
43            'hello': 'world',
44            'foo': 'bar',
45        }
46
47        result = dict_deep_merge(dict_b, dict_a, lambda: dict())
48        self.assertEqual(result, expected)
49
50    def test_invariants_with_dict_implicit_ctor_success(self):
51        dict_a = {'hello': 'world'}
52        dict_b = {'foo': 'bar'}
53
54        expected = {
55            'hello': 'world',
56            'foo': 'bar',
57        }
58
59        result = dict_deep_merge(dict_b, dict_a)
60        self.assertEqual(result, expected)
61
62    def test_invariants_with_dict_fails_wrong_ctor_type(self):
63        dict_a = {'hello': 'world'}
64        dict_b = {'foo': 'bar'}
65
66        with self.assertRaises(TypeError):
67            dict_deep_merge(dict_b, dict_a, lambda: OrderedDict())
68
69    def test_invariants_with_ordered_dict_success(self):
70        dict_a = OrderedDict({'hello': 'world'})
71        dict_b = OrderedDict({'foo': 'bar'})
72
73        expected = OrderedDict(
74            {
75                'hello': 'world',
76                'foo': 'bar',
77            }
78        )
79
80        result = dict_deep_merge(dict_b, dict_a, lambda: OrderedDict())
81        self.assertEqual(result, expected)
82
83    def test_invariants_with_ordered_dict_implicit_ctor_success(self):
84        dict_a = OrderedDict({'hello': 'world'})
85        dict_b = OrderedDict({'foo': 'bar'})
86
87        expected = OrderedDict(
88            {
89                'hello': 'world',
90                'foo': 'bar',
91            }
92        )
93
94        result = dict_deep_merge(dict_b, dict_a)
95        self.assertEqual(result, expected)
96
97    def test_invariants_with_ordered_dict_fails_wrong_ctor_type(self):
98        dict_a = OrderedDict({'hello': 'world'})
99        dict_b = OrderedDict({'foo': 'bar'})
100
101        with self.assertRaises(TypeError):
102            dict_deep_merge(dict_b, dict_a, lambda: dict())
103
104    # pylint: enable=unnecessary-lambda
105
106    def test_merge_basic(self):
107        dict_a = {'hello': 'world'}
108        dict_b = {'hello': 'bar'}
109
110        expected = {'hello': 'bar'}
111        result = dict_deep_merge(dict_b, dict_a)
112        self.assertEqual(result, expected)
113
114    def test_merge_nested_dict(self):
115        dict_a = {'hello': {'a': 'foo'}}
116        dict_b = {'hello': {'b': 'bar'}}
117
118        expected = {'hello': {'a': 'foo', 'b': 'bar'}}
119        result = dict_deep_merge(dict_b, dict_a)
120        self.assertEqual(result, expected)
121
122    def test_merge_list(self):
123        dict_a = {'hello': ['world']}
124        dict_b = {'hello': ['bar']}
125
126        expected = {'hello': ['world', 'bar']}
127        result = dict_deep_merge(dict_b, dict_a)
128        self.assertEqual(result, expected)
129
130    def test_merge_list_no_duplicates(self):
131        dict_a = {'hello': ['world']}
132        dict_b = {'hello': ['world']}
133
134        expected = {'hello': ['world']}
135        result = dict_deep_merge(dict_b, dict_a)
136        self.assertEqual(result, expected)
137
138    def test_merge_nested_dict_with_lists(self):
139        dict_a = {'hello': {'a': 'foo', 'c': ['lorem']}}
140        dict_b = {'hello': {'b': 'bar', 'c': ['ipsum']}}
141
142        expected = {'hello': {'a': 'foo', 'b': 'bar', 'c': ['lorem', 'ipsum']}}
143        result = dict_deep_merge(dict_b, dict_a)
144        self.assertEqual(result, expected)
145
146    def test_merge_object_fails(self):
147        class Strawman:
148            pass
149
150        dict_a = {'hello': 'world'}
151        dict_b = {'foo': Strawman()}
152
153        with self.assertRaises(TypeError):
154            dict_deep_merge(dict_b, dict_a)
155
156    def test_merge_copies_string(self):
157        test_str = 'bar'
158        dict_a = {'hello': {'a': 'foo'}}
159        dict_b = {'hello': {'b': test_str}}
160
161        result = dict_deep_merge(dict_b, dict_a)
162        test_str = 'something else'
163
164        self.assertEqual(result['hello']['b'], 'bar')
165
166
167class TestDictSwapType(unittest.TestCase):
168    """Tests dict_swap_type"""
169
170    def test_ordereddict_to_dict(self):
171        """Test converting an OrderedDict to a plain dict"""
172
173        ordered_dict = OrderedDict(
174            {
175                'hello': 'world',
176                'foo': 'bar',
177                'nested': OrderedDict(
178                    {
179                        'lorem': 'ipsum',
180                        'dolor': 'sit amet',
181                    }
182                ),
183            }
184        )
185
186        plain_dict = dict_swap_type(ordered_dict, dict)
187
188        expected_plain_dict = {
189            'hello': 'world',
190            'foo': 'bar',
191            'nested': {
192                'lorem': 'ipsum',
193                'dolor': 'sit amet',
194            },
195        }
196
197        # The returned dict has the content and type we expect
198        self.assertDictEqual(plain_dict, expected_plain_dict)
199        self.assertIsInstance(plain_dict, dict)
200        self.assertIsInstance(plain_dict['nested'], dict)
201
202        # The original OrderedDict is unchanged
203        self.assertIsInstance(ordered_dict, OrderedDict)
204        self.assertIsInstance(ordered_dict['nested'], OrderedDict)
205
206
207class EditorSettingsTestType(Enum):
208    SETTINGS = 'settings'
209
210
211class TestCasesGenericOnFileFormat:
212    """Container for tests generic on FileFormat.
213
214    This misdirection is needed to prevent the base test class cases from being
215    run as actual tests.
216    """
217
218    class EditorSettingsFileTestCase(PwIdeTestCase):
219        """Test case for EditorSettingsFile with a provided FileFormat"""
220
221        def setUp(self):
222            if not hasattr(self, 'file_format'):
223                self.file_format = _StructuredFileFormat()
224            return super().setUp()
225
226        def test_open_new_file_and_write(self):
227            name = 'settings'
228            settings_file = EditorSettingsFile(
229                self.temp_dir_path, name, self.file_format
230            )
231
232            with settings_file.build() as settings:
233                settings['hello'] = 'world'
234
235            with open(
236                self.temp_dir_path / f'{name}.{self.file_format.ext}'
237            ) as file:
238                settings_dict = self.file_format.load(file)
239
240            self.assertEqual(settings_dict['hello'], 'world')
241
242        def test_open_new_file_and_get(self):
243            name = 'settings'
244            settings_file = EditorSettingsFile(
245                self.temp_dir_path, name, self.file_format
246            )
247
248            with settings_file.build() as settings:
249                settings['hello'] = 'world'
250
251            settings_dict = settings_file.get()
252            self.assertEqual(settings_dict['hello'], 'world')
253
254    class EditorSettingsManagerTestCase(PwIdeTestCase):
255        """Test case for EditorSettingsManager with a provided FileFormat"""
256
257        def setUp(self):
258            if not hasattr(self, 'file_format'):
259                self.file_format = _StructuredFileFormat()
260            return super().setUp()
261
262        def test_settings_merge(self):
263            """Test that settings merge as expected in isolation."""
264            default_settings = OrderedDict(
265                {
266                    'foo': 'bar',
267                    'baz': 'qux',
268                    'lorem': OrderedDict(
269                        {
270                            'ipsum': 'dolor',
271                        }
272                    ),
273                }
274            )
275
276            types_with_defaults = {
277                EditorSettingsTestType.SETTINGS: lambda _: default_settings
278            }
279
280            ide_settings = self.make_ide_settings()
281            manager = EditorSettingsManager(
282                ide_settings,
283                self.temp_dir_path,
284                self.file_format,
285                types_with_defaults,
286            )
287
288            project_settings = OrderedDict(
289                {
290                    'alpha': 'beta',
291                    'baz': 'xuq',
292                    'foo': 'rab',
293                }
294            )
295
296            with manager.project(
297                EditorSettingsTestType.SETTINGS
298            ).build() as settings:
299                dict_deep_merge(project_settings, settings)
300
301            user_settings = OrderedDict(
302                {
303                    'baz': 'xqu',
304                    'lorem': OrderedDict(
305                        {
306                            'ipsum': 'sit amet',
307                            'consectetur': 'adipiscing',
308                        }
309                    ),
310                }
311            )
312
313            with manager.user(
314                EditorSettingsTestType.SETTINGS
315            ).build() as settings:
316                dict_deep_merge(user_settings, settings)
317
318            expected = {
319                'alpha': 'beta',
320                'foo': 'rab',
321                'baz': 'xqu',
322                'lorem': {
323                    'ipsum': 'sit amet',
324                    'consectetur': 'adipiscing',
325                },
326            }
327
328            with manager.active(
329                EditorSettingsTestType.SETTINGS
330            ).build() as active_settings:
331                manager.default(EditorSettingsTestType.SETTINGS).sync_to(
332                    active_settings
333                )
334                manager.project(EditorSettingsTestType.SETTINGS).sync_to(
335                    active_settings
336                )
337                manager.user(EditorSettingsTestType.SETTINGS).sync_to(
338                    active_settings
339                )
340
341            self.assertCountEqual(
342                manager.active(EditorSettingsTestType.SETTINGS).get(), expected
343            )
344
345
346class TestEditorSettingsFileJsonFormat(
347    TestCasesGenericOnFileFormat.EditorSettingsFileTestCase
348):
349    """Test EditorSettingsFile with JsonFormat"""
350
351    def setUp(self):
352        self.file_format = JsonFileFormat()
353        return super().setUp()
354
355
356class TestEditorSettingsManagerJsonFormat(
357    TestCasesGenericOnFileFormat.EditorSettingsManagerTestCase
358):
359    """Test EditorSettingsManager with JsonFormat"""
360
361    def setUp(self):
362        self.file_format = JsonFileFormat()
363        return super().setUp()
364
365
366class TestEditorSettingsFileJson5Format(
367    TestCasesGenericOnFileFormat.EditorSettingsFileTestCase
368):
369    """Test EditorSettingsFile with Json5Format"""
370
371    def setUp(self):
372        self.file_format = Json5FileFormat()
373        return super().setUp()
374
375
376class TestEditorSettingsManagerJson5Format(
377    TestCasesGenericOnFileFormat.EditorSettingsManagerTestCase
378):
379    """Test EditorSettingsManager with Json5Format"""
380
381    def setUp(self):
382        self.file_format = Json5FileFormat()
383        return super().setUp()
384
385
386class TestEditorSettingsFileYamlFormat(
387    TestCasesGenericOnFileFormat.EditorSettingsFileTestCase
388):
389    """Test EditorSettingsFile with YamlFormat"""
390
391    def setUp(self):
392        self.file_format = YamlFileFormat()
393        return super().setUp()
394
395
396class TestEditorSettingsManagerYamlFormat(
397    TestCasesGenericOnFileFormat.EditorSettingsManagerTestCase
398):
399    """Test EditorSettingsManager with YamlFormat"""
400
401    def setUp(self):
402        self.file_format = YamlFileFormat()
403        return super().setUp()
404
405
406if __name__ == '__main__':
407    unittest.main()
408