test_dictupdate.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. # -*- coding: utf-8 -*-
  2. # Import python libs
  3. from __future__ import absolute_import, print_function, unicode_literals
  4. import copy
  5. # Import Salt libs
  6. import salt.utils.dictupdate as dictupdate
  7. from salt.exceptions import SaltInvocationError
  8. from salt.utils.odict import OrderedDict
  9. # Import Salt Testing libs
  10. from tests.support.unit import TestCase
  11. class UtilDictupdateTestCase(TestCase):
  12. dict1 = {"A": "B", "C": {"D": "E", "F": {"G": "H", "I": "J"}}}
  13. def test_update(self):
  14. # level 1 value changes
  15. mdict = copy.deepcopy(self.dict1)
  16. mdict["A"] = "Z"
  17. res = dictupdate.update(copy.deepcopy(self.dict1), {"A": "Z"})
  18. self.assertEqual(res, mdict)
  19. # level 1 value changes (list replacement)
  20. mdict = copy.deepcopy(self.dict1)
  21. mdict["A"] = [1, 2]
  22. res = dictupdate.update(copy.deepcopy(mdict), {"A": [2, 3]}, merge_lists=False)
  23. mdict["A"] = [2, 3]
  24. self.assertEqual(res, mdict)
  25. # level 1 value changes (list merge)
  26. mdict = copy.deepcopy(self.dict1)
  27. mdict["A"] = [1, 2]
  28. res = dictupdate.update(copy.deepcopy(mdict), {"A": [3, 4]}, merge_lists=True)
  29. mdict["A"] = [1, 2, 3, 4]
  30. self.assertEqual(res, mdict)
  31. # level 1 value changes (list merge, remove duplicates, preserve order)
  32. mdict = copy.deepcopy(self.dict1)
  33. mdict["A"] = [1, 2]
  34. res = dictupdate.update(
  35. copy.deepcopy(mdict), {"A": [4, 3, 2, 1]}, merge_lists=True
  36. )
  37. mdict["A"] = [1, 2, 4, 3]
  38. self.assertEqual(res, mdict)
  39. # level 2 value changes
  40. mdict = copy.deepcopy(self.dict1)
  41. mdict["C"]["D"] = "Z"
  42. res = dictupdate.update(copy.deepcopy(self.dict1), {"C": {"D": "Z"}})
  43. self.assertEqual(res, mdict)
  44. # level 2 value changes (list replacement)
  45. mdict = copy.deepcopy(self.dict1)
  46. mdict["C"]["D"] = ["a", "b"]
  47. res = dictupdate.update(
  48. copy.deepcopy(mdict), {"C": {"D": ["c", "d"]}}, merge_lists=False
  49. )
  50. mdict["C"]["D"] = ["c", "d"]
  51. self.assertEqual(res, mdict)
  52. # level 2 value changes (list merge)
  53. mdict = copy.deepcopy(self.dict1)
  54. mdict["C"]["D"] = ["a", "b"]
  55. res = dictupdate.update(
  56. copy.deepcopy(mdict), {"C": {"D": ["c", "d"]}}, merge_lists=True
  57. )
  58. mdict["C"]["D"] = ["a", "b", "c", "d"]
  59. self.assertEqual(res, mdict)
  60. # level 2 value changes (list merge, remove duplicates, preserve order)
  61. mdict = copy.deepcopy(self.dict1)
  62. mdict["C"]["D"] = ["a", "b"]
  63. res = dictupdate.update(
  64. copy.deepcopy(mdict), {"C": {"D": ["d", "c", "b", "a"]}}, merge_lists=True
  65. )
  66. mdict["C"]["D"] = ["a", "b", "d", "c"]
  67. self.assertEqual(res, mdict)
  68. # level 3 value changes
  69. mdict = copy.deepcopy(self.dict1)
  70. mdict["C"]["F"]["G"] = "Z"
  71. res = dictupdate.update(copy.deepcopy(self.dict1), {"C": {"F": {"G": "Z"}}})
  72. self.assertEqual(res, mdict)
  73. # level 3 value changes (list replacement)
  74. mdict = copy.deepcopy(self.dict1)
  75. mdict["C"]["F"]["G"] = ["a", "b"]
  76. res = dictupdate.update(
  77. copy.deepcopy(mdict), {"C": {"F": {"G": ["c", "d"]}}}, merge_lists=False
  78. )
  79. mdict["C"]["F"]["G"] = ["c", "d"]
  80. self.assertEqual(res, mdict)
  81. # level 3 value changes (list merge)
  82. mdict = copy.deepcopy(self.dict1)
  83. mdict["C"]["F"]["G"] = ["a", "b"]
  84. res = dictupdate.update(
  85. copy.deepcopy(mdict), {"C": {"F": {"G": ["c", "d"]}}}, merge_lists=True
  86. )
  87. mdict["C"]["F"]["G"] = ["a", "b", "c", "d"]
  88. self.assertEqual(res, mdict)
  89. # level 3 value changes (list merge, remove duplicates, preserve order)
  90. mdict = copy.deepcopy(self.dict1)
  91. mdict["C"]["F"]["G"] = ["a", "b"]
  92. res = dictupdate.update(
  93. copy.deepcopy(mdict),
  94. {"C": {"F": {"G": ["d", "c", "b", "a"]}}},
  95. merge_lists=True,
  96. )
  97. mdict["C"]["F"]["G"] = ["a", "b", "d", "c"]
  98. self.assertEqual(res, mdict)
  99. # replace a sub-dictionary
  100. mdict = copy.deepcopy(self.dict1)
  101. mdict["C"] = "Z"
  102. res = dictupdate.update(copy.deepcopy(self.dict1), {"C": "Z"})
  103. self.assertEqual(res, mdict)
  104. # add a new scalar value
  105. mdict = copy.deepcopy(self.dict1)
  106. mdict["Z"] = "Y"
  107. res = dictupdate.update(copy.deepcopy(self.dict1), {"Z": "Y"})
  108. self.assertEqual(res, mdict)
  109. # add a dictionary
  110. mdict = copy.deepcopy(self.dict1)
  111. mdict["Z"] = {"Y": "X"}
  112. res = dictupdate.update(copy.deepcopy(self.dict1), {"Z": {"Y": "X"}})
  113. self.assertEqual(res, mdict)
  114. # add a nested dictionary
  115. mdict = copy.deepcopy(self.dict1)
  116. mdict["Z"] = {"Y": {"X": "W"}}
  117. res = dictupdate.update(copy.deepcopy(self.dict1), {"Z": {"Y": {"X": "W"}}})
  118. self.assertEqual(res, mdict)
  119. class UtilDictMergeTestCase(TestCase):
  120. dict1 = {"A": "B", "C": {"D": "E", "F": {"G": "H", "I": "J"}}}
  121. def test_merge_overwrite_traditional(self):
  122. """
  123. Test traditional overwrite, wherein a key in the second dict overwrites a key in the first
  124. """
  125. mdict = copy.deepcopy(self.dict1)
  126. mdict["A"] = "b"
  127. ret = dictupdate.merge_overwrite(copy.deepcopy(self.dict1), {"A": "b"})
  128. self.assertEqual(mdict, ret)
  129. def test_merge_overwrite_missing_source_key(self):
  130. """
  131. Test case wherein the overwrite strategy is used but a key in the second dict is
  132. not present in the first
  133. """
  134. mdict = copy.deepcopy(self.dict1)
  135. mdict["D"] = "new"
  136. ret = dictupdate.merge_overwrite(copy.deepcopy(self.dict1), {"D": "new"})
  137. self.assertEqual(mdict, ret)
  138. def test_merge_aggregate_traditional(self):
  139. """
  140. Test traditional aggregation, where a val from dict2 overwrites one
  141. present in dict1
  142. """
  143. mdict = copy.deepcopy(self.dict1)
  144. mdict["A"] = "b"
  145. ret = dictupdate.merge_overwrite(copy.deepcopy(self.dict1), {"A": "b"})
  146. self.assertEqual(mdict, ret)
  147. def test_merge_list_traditional(self):
  148. """
  149. Test traditional list merge, where a key present in dict2 will be converted
  150. to a list
  151. """
  152. mdict = copy.deepcopy(self.dict1)
  153. mdict["A"] = ["B", "b"]
  154. ret = dictupdate.merge_list(copy.deepcopy(self.dict1), {"A": "b"})
  155. self.assertEqual(mdict, ret)
  156. def test_merge_list_append(self):
  157. """
  158. This codifies the intended behaviour that items merged into a dict val that is already
  159. a list that those items will *appended* to the list, and not magically merged in
  160. """
  161. mdict = copy.deepcopy(self.dict1)
  162. mdict["A"] = ["B", "b", "c"]
  163. # Prepare a modified copy of dict1 that has a list as a val for the key of 'A'
  164. mdict1 = copy.deepcopy(self.dict1)
  165. mdict1["A"] = ["B"]
  166. ret = dictupdate.merge_list(mdict1, {"A": ["b", "c"]})
  167. self.assertEqual(
  168. {"A": [["B"], ["b", "c"]], "C": {"D": "E", "F": {"I": "J", "G": "H"}}}, ret
  169. )
  170. class UtilDeepDictUpdateTestCase(TestCase):
  171. dict1 = {"A": "B", "C": {"D": "E", "F": {"G": "H", "I": "J"}}}
  172. def test_deep_set_overwrite(self):
  173. """
  174. Test overwriting an existing value.
  175. """
  176. mdict = copy.deepcopy(self.dict1)
  177. res = dictupdate.set_dict_key_value(mdict, "C:F", "foo")
  178. self.assertEqual({"A": "B", "C": {"D": "E", "F": "foo"}}, res)
  179. # Verify modify-in-place
  180. self.assertEqual({"A": "B", "C": {"D": "E", "F": "foo"}}, mdict)
  181. # Test using alternative delimiter
  182. res = dictupdate.set_dict_key_value(
  183. mdict, "C/F", {"G": "H", "I": "J"}, delimiter="/"
  184. )
  185. self.assertEqual(self.dict1, res)
  186. # Test without using a delimiter in the keys
  187. res = dictupdate.set_dict_key_value(mdict, "C", None)
  188. self.assertEqual({"A": "B", "C": None}, res)
  189. def test_deep_set_create(self):
  190. """
  191. Test creating new nested keys.
  192. """
  193. mdict = copy.deepcopy(self.dict1)
  194. res = dictupdate.set_dict_key_value(mdict, "K:L:M", "Q")
  195. self.assertEqual(
  196. {
  197. "A": "B",
  198. "C": {"D": "E", "F": {"G": "H", "I": "J"}},
  199. "K": {"L": {"M": "Q"}},
  200. },
  201. res,
  202. )
  203. def test_deep_set_ordered_dicts(self):
  204. """
  205. Test creating new nested ordereddicts.
  206. """
  207. res = dictupdate.set_dict_key_value({}, "A:B", "foo", ordered_dict=True)
  208. self.assertEqual({"A": OrderedDict([("B", "foo")])}, res)
  209. def test_deep_append(self):
  210. """
  211. Test appending to a list.
  212. """
  213. sdict = {"bar": {"baz": [1, 2]}}
  214. res = dictupdate.append_dict_key_value(sdict, "bar:baz", 42)
  215. self.assertEqual({"bar": {"baz": [1, 2, 42]}}, res)
  216. # Append with alternate delimiter
  217. res = dictupdate.append_dict_key_value(sdict, "bar~baz", 43, delimiter="~")
  218. self.assertEqual({"bar": {"baz": [1, 2, 42, 43]}}, res)
  219. # Append to a not-yet existing list
  220. res = dictupdate.append_dict_key_value({}, "foo:bar:baz", 42)
  221. self.assertEqual({"foo": {"bar": {"baz": [42]}}}, res)
  222. def test_deep_extend(self):
  223. """
  224. Test extending a list.
  225. Note that the provided value (to extend with) will be coerced to a list
  226. if this is not already a list. This can cause unexpected behaviour.
  227. """
  228. sdict = {"bar": {"baz": [1, 2]}}
  229. res = dictupdate.extend_dict_key_value(sdict, "bar:baz", [42, 42])
  230. self.assertEqual({"bar": {"baz": [1, 2, 42, 42]}}, res)
  231. # Extend a not-yet existing list
  232. res = dictupdate.extend_dict_key_value({}, "bar:baz:qux", [42])
  233. self.assertEqual({"bar": {"baz": {"qux": [42]}}}, res)
  234. # Extend with a dict (remember, foo has been updated in the first test)
  235. res = dictupdate.extend_dict_key_value(sdict, "bar:baz", {"qux": "quux"})
  236. self.assertEqual({"bar": {"baz": [1, 2, 42, 42, "qux"]}}, res)
  237. def test_deep_extend_illegal_addition(self):
  238. """
  239. Test errorhandling extending lists with illegal types.
  240. """
  241. # Extend with an illegal type
  242. for extend_with in [42, None]:
  243. with self.assertRaisesRegex(
  244. SaltInvocationError,
  245. r"Cannot extend {} with a {}." "".format(type([]), type(extend_with)),
  246. ):
  247. dictupdate.extend_dict_key_value({}, "foo", extend_with)
  248. def test_deep_extend_illegal_source(self):
  249. """
  250. Test errorhandling extending things that are not a list.
  251. """
  252. # Extend an illegal type
  253. for extend_this in [{}, 42, "bar"]:
  254. with self.assertRaisesRegex(
  255. SaltInvocationError,
  256. r"The last key contains a {}, which cannot extend."
  257. "".format(type(extend_this)),
  258. ):
  259. dictupdate.extend_dict_key_value({"foo": extend_this}, "foo", [42])
  260. def test_deep_update(self):
  261. """
  262. Test updating a (sub)dict.
  263. """
  264. mdict = copy.deepcopy(self.dict1)
  265. res = dictupdate.update_dict_key_value(
  266. mdict, "C:F", {"foo": "bar", "qux": "quux"}
  267. )
  268. self.assertEqual(
  269. {
  270. "A": "B",
  271. "C": {"D": "E", "F": {"G": "H", "I": "J", "foo": "bar", "qux": "quux"}},
  272. },
  273. res,
  274. )
  275. # Test updating a non-existing subkey
  276. res = dictupdate.update_dict_key_value({}, "foo:bar:baz", {"qux": "quux"})
  277. self.assertEqual({"foo": {"bar": {"baz": {"qux": "quux"}}}}, res)
  278. # Test updating a non-existing subkey, with a different delimiter
  279. res = dictupdate.update_dict_key_value(
  280. {}, "foo bar baz", {"qux": "quux"}, delimiter=" "
  281. )
  282. self.assertEqual({"foo": {"bar": {"baz": {"qux": "quux"}}}}, res)
  283. def test_deep_update_illegal_update(self):
  284. """
  285. Test errorhandling updating a (sub)dict with illegal types.
  286. """
  287. # Update with an illegal type
  288. for update_with in [42, None, [42], "bar"]:
  289. with self.assertRaisesRegex(
  290. SaltInvocationError,
  291. r"Cannot update {} with a {}." "".format(type({}), type(update_with)),
  292. ):
  293. dictupdate.update_dict_key_value({}, "foo", update_with)
  294. # Again, but now using OrderedDicts
  295. for update_with in [42, None, [42], "bar"]:
  296. with self.assertRaisesRegex(
  297. SaltInvocationError,
  298. r"Cannot update {} with a {}."
  299. "".format(type(OrderedDict()), type(update_with)),
  300. ):
  301. dictupdate.update_dict_key_value(
  302. {}, "foo", update_with, ordered_dict=True
  303. )