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